From c52531b37a8f03454bca93b40402c861c506bb81 Mon Sep 17 00:00:00 2001 From: Alexander Medvedev Date: Sun, 21 Jun 2026 07:55:09 +0200 Subject: [PATCH] feat: Implement Trident --- pumpkin/src/entity/player.rs | 64 +++- pumpkin/src/entity/projectile/arrow.rs | 23 +- pumpkin/src/entity/projectile/mod.rs | 2 + pumpkin/src/entity/projectile/trident.rs | 460 +++++++++++++++++++++++ pumpkin/src/entity/type.rs | 2 + pumpkin/src/item/items/bow.rs | 39 +- pumpkin/src/item/items/trident.rs | 162 +++++++- 7 files changed, 729 insertions(+), 23 deletions(-) create mode 100644 pumpkin/src/entity/projectile/trident.rs diff --git a/pumpkin/src/entity/player.rs b/pumpkin/src/entity/player.rs index 2235f0c29..20604c5dd 100644 --- a/pumpkin/src/entity/player.rs +++ b/pumpkin/src/entity/player.rs @@ -918,20 +918,57 @@ impl Player { let mut damage_multiplier = 1.0; let mut add_damage = 0.0; let mut add_speed = 0.0; + let mut extra_ench_damage = 0.0; + let mut knockback_level = 0u32; - // Get the attack damage from the held item - // TODO: this should be cached in memory, we shouldn't just use default here either - if let Some(modifiers) = item_stack - .lock() - .await - .get_data_component::() { - for item_mod in modifiers.attribute_modifiers.iter() { - if item_mod.operation == Operation::AddValue { - if item_mod.id == "minecraft:base_attack_damage" { - add_damage = item_mod.amount; - } else if item_mod.id == "minecraft:base_attack_speed" { - add_speed = item_mod.amount; + let stack = item_stack.lock().await; + if let Some(modifiers) = stack.get_data_component::() { + for item_mod in modifiers.attribute_modifiers.iter() { + if item_mod.operation == Operation::AddValue { + if item_mod.id == "minecraft:base_attack_damage" { + add_damage = item_mod.amount; + } else if item_mod.id == "minecraft:base_attack_speed" { + add_speed = item_mod.amount; + } + } + } + } + if let Some(enchantments) = stack.get_data_component::() { + for (enchantment, level) in enchantments.enchantment.iter() { + if **enchantment == Enchantment::SHARPNESS { + extra_ench_damage += 0.5 * f64::from(*level) + 0.5; + } else if **enchantment == Enchantment::SMITE { + let target_type = victim_entity.entity_type.id; + let is_undead = target_type == EntityType::ZOMBIE.id + || target_type == EntityType::DROWNED.id + || target_type == EntityType::HUSK.id + || target_type == EntityType::ZOMBIE_VILLAGER.id + || target_type == EntityType::ZOMBIFIED_PIGLIN.id + || target_type == EntityType::SKELETON.id + || target_type == EntityType::BOGGED.id + || target_type == EntityType::PARCHED.id + || target_type == EntityType::WITHER_SKELETON.id + || target_type == EntityType::STRAY.id + || target_type == EntityType::PHANTOM.id + || target_type == EntityType::WITHER.id + || target_type == EntityType::ZOMBIE_HORSE.id + || target_type == EntityType::SKELETON_HORSE.id; + if is_undead { + extra_ench_damage += 2.5 * f64::from(*level); + } + } else if **enchantment == Enchantment::BANE_OF_ARTHROPODS { + let target_type = victim_entity.entity_type.id; + let is_arthropod = target_type == EntityType::SPIDER.id + || target_type == EntityType::CAVE_SPIDER.id + || target_type == EntityType::SILVERFISH.id + || target_type == EntityType::ENDERMITE.id + || target_type == EntityType::BEE.id; + if is_arthropod { + extra_ench_damage += 2.5 * f64::from(*level); + } + } else if **enchantment == Enchantment::KNOCKBACK { + knockback_level = *level as u32; } } } @@ -954,6 +991,7 @@ impl Player { // Modify the added damage based on the multiplier. let mut damage = base_damage + add_damage * damage_multiplier; + damage += extra_ench_damage * attack_cooldown_progress; if let Some(strength) = self .living_entity @@ -1048,7 +1086,7 @@ impl Player { ); if victim.get_living_entity().is_some() { - let mut knockback_strength = 1.0; + let mut knockback_strength = 1.0 + f64::from(knockback_level); match attack_type { AttackType::Knockback => knockback_strength += 1.0, AttackType::Sweeping => { diff --git a/pumpkin/src/entity/projectile/arrow.rs b/pumpkin/src/entity/projectile/arrow.rs index 145e60ffe..3d57dcf32 100644 --- a/pumpkin/src/entity/projectile/arrow.rs +++ b/pumpkin/src/entity/projectile/arrow.rs @@ -55,6 +55,8 @@ pub struct ArrowEntity { pub pickup: ArrowPickup, pub is_critical: AtomicBool, pub pierce_level: AtomicU8, + pub punch_level: AtomicU8, + pub is_flame: AtomicBool, pub in_ground: AtomicBool, pub in_ground_time: AtomicU32, pub life: AtomicU32, @@ -78,6 +80,8 @@ impl ArrowEntity { pickup: ArrowPickup::Disallowed, is_critical: AtomicBool::new(false), pierce_level: AtomicU8::new(0), + punch_level: AtomicU8::new(0), + is_flame: AtomicBool::new(false), in_ground: AtomicBool::new(false), in_ground_time: AtomicU32::new(0), life: AtomicU32::new(0), @@ -100,6 +104,8 @@ impl ArrowEntity { pickup, is_critical: AtomicBool::new(false), pierce_level: AtomicU8::new(0), + punch_level: AtomicU8::new(0), + is_flame: AtomicBool::new(false), in_ground: AtomicBool::new(false), in_ground_time: AtomicU32::new(0), life: AtomicU32::new(0), @@ -390,12 +396,27 @@ impl EntityBase for ArrowEntity { let bonus = (rand::random::() % (damage / 2 + 2) as u32) as i32; damage = damage.saturating_add(bonus); } + if self.is_flame.load(Ordering::Relaxed) { + target.get_entity().set_on_fire_for_ticks(100); + } + target .damage(&*target, damage as f32, DamageType::ARROW) .await; if target.get_living_entity().is_some() { - // TODO: knockback + let punch = self.punch_level.load(Ordering::Relaxed); + if punch > 0 { + if let Some(owner_id) = self.owner_id { + if let Some(owner_entity) = world.get_entity_by_id(owner_id) { + crate::entity::combat::handle_knockback( + owner_entity.get_entity(), + target.get_entity(), + f64::from(punch) * 0.6, + ); + } + } + } // Play hit sound let sound_packet = CSoundEffect::new( diff --git a/pumpkin/src/entity/projectile/mod.rs b/pumpkin/src/entity/projectile/mod.rs index 9208e8a10..1dd6e388e 100644 --- a/pumpkin/src/entity/projectile/mod.rs +++ b/pumpkin/src/entity/projectile/mod.rs @@ -21,11 +21,13 @@ pub mod shulker_bullet; pub mod small_fireball; pub mod snowball; pub mod splash_potion; +pub mod trident; pub mod wind_charge; #[must_use] pub fn is_projectile(entity_type: &EntityType) -> bool { *entity_type == EntityType::ARROW + || *entity_type == EntityType::TRIDENT || *entity_type == EntityType::EGG || *entity_type == EntityType::SNOWBALL || *entity_type == EntityType::FIREWORK_ROCKET diff --git a/pumpkin/src/entity/projectile/trident.rs b/pumpkin/src/entity/projectile/trident.rs new file mode 100644 index 000000000..78ca46219 --- /dev/null +++ b/pumpkin/src/entity/projectile/trident.rs @@ -0,0 +1,460 @@ +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, AtomicU8, AtomicU32, Ordering}; +use tokio::sync::Mutex; + +use crate::{ + entity::{ + Entity, EntityBase, EntityBaseFuture, NBTStorage, living::LivingEntity, player::Player, + }, + server::Server, +}; +use pumpkin_data::damage::DamageType; +use pumpkin_data::item::Item; +use pumpkin_data::item_stack::ItemStack; +use pumpkin_data::sound::{Sound, SoundCategory}; +use pumpkin_protocol::IdOr; +use pumpkin_protocol::java::client::play::{CEntityVelocity, CSoundEffect}; +use pumpkin_util::math::boundingbox::BoundingBox; +use pumpkin_util::math::position::BlockPos; +use pumpkin_util::math::vector3::Vector3; + +use super::ProjectileHit; +use super::arrow::ArrowPickup; + +pub struct TridentEntity { + pub entity: Entity, + pub owner_id: Option, + pub item_stack: Arc>, + pub pickup: ArrowPickup, + pub in_ground: AtomicBool, + pub in_ground_time: AtomicU32, + pub life: AtomicU32, + pub shake_time: AtomicU8, + pub has_hit: AtomicBool, + pub last_block_pos: Arc>>, +} + +impl TridentEntity { + const BASE_DAMAGE: f64 = 8.0; + const AIR_INERTIA: f64 = 0.99; + const WATER_INERTIA: f64 = 0.9; + const GRAVITY: f64 = 0.05; + const DESPAWN_TIME: u32 = 1200; + + pub fn new(entity: Entity, owner_id: Option) -> Self { + Self { + entity, + owner_id, + item_stack: Arc::new(Mutex::new(ItemStack::new(1, &Item::TRIDENT))), + pickup: ArrowPickup::Disallowed, + in_ground: AtomicBool::new(false), + in_ground_time: AtomicU32::new(0), + life: AtomicU32::new(0), + shake_time: AtomicU8::new(0), + has_hit: AtomicBool::new(false), + last_block_pos: Arc::new(std::sync::RwLock::new(None)), + } + } + + pub fn new_shot( + entity: Entity, + shooter: &Entity, + item_stack: ItemStack, + pickup: ArrowPickup, + ) -> Self { + let mut owner_pos = shooter.pos.load(); + owner_pos.y = owner_pos.y + f64::from(shooter.entity_dimension.load().eye_height) - 0.1; + entity.pos.store(owner_pos); + entity.set_velocity(Vector3::new(0.0, 0.1, 0.0)); + + Self { + entity, + owner_id: Some(shooter.entity_id), + item_stack: Arc::new(Mutex::new(item_stack)), + pickup, + in_ground: AtomicBool::new(false), + in_ground_time: AtomicU32::new(0), + life: AtomicU32::new(0), + shake_time: AtomicU8::new(0), + has_hit: AtomicBool::new(false), + last_block_pos: Arc::new(std::sync::RwLock::new(None)), + } + } + + pub fn set_velocity_from_rotation( + &self, + pitch: f32, + yaw: f32, + roll: f32, + speed: f32, + divergence: f32, + ) { + let yaw_rad = yaw.to_radians(); + let pitch_rad = pitch.to_radians(); + let roll_rad = (pitch + roll).to_radians(); + + let x = -yaw_rad.sin() * pitch_rad.cos(); + let y = -roll_rad.sin(); + let z = yaw_rad.cos() * pitch_rad.cos(); + + self.set_velocity( + f64::from(x), + f64::from(y), + f64::from(z), + f64::from(speed), + f64::from(divergence), + ); + } + + pub fn set_velocity(&self, x: f64, y: f64, z: f64, power: f64, uncertainty: f64) { + fn next_triangular(mode: f64, deviation: f64) -> f64 { + deviation.mul_add(rand::random::() - rand::random::(), mode) + } + + let velocity = Vector3::new(x, y, z) + .normalize() + .add_raw( + next_triangular(0.0, 0.017_227_5 * uncertainty), + next_triangular(0.0, 0.017_227_5 * uncertainty), + next_triangular(0.0, 0.017_227_5 * uncertainty), + ) + .multiply(power, power, power); + + self.entity.velocity.store(velocity); + let len = velocity.horizontal_length(); + self.entity.set_rotation( + velocity.x.atan2(velocity.z) as f32 * 57.295_776, + velocity.y.atan2(len) as f32 * 57.295_776, + ); + } + + fn should_skip_collision(&self, self_ent: &Entity, other: &Arc) -> bool { + let other_ent = other.get_entity(); + + // Don't collide with self + if other_ent.entity_id == self_ent.entity_id { + return true; + } + + // Skip owner for initial frames (5 ticks) + if Some(other_ent.entity_id) == self.owner_id && self_ent.age.load(Ordering::Relaxed) < 5 { + return true; + } + + // Skip other projectiles and item entities + if other_ent.entity_type == &pumpkin_data::entity::EntityType::ARROW + || other_ent.entity_type == &pumpkin_data::entity::EntityType::TRIDENT + || other_ent.entity_type == &pumpkin_data::entity::EntityType::ITEM + || other_ent.entity_type == &pumpkin_data::entity::EntityType::FALLING_BLOCK + { + return true; + } + + false + } +} + +impl NBTStorage for TridentEntity {} + +impl EntityBase for TridentEntity { + fn tick<'a>( + &'a self, + caller: &'a Arc, + _server: &'a Server, + ) -> EntityBaseFuture<'a, ()> { + Box::pin(async move { + let entity = self.get_entity(); + let world = entity.world.load(); + + // Handle shake time + let shake = self.shake_time.load(Ordering::Relaxed); + if shake > 0 { + self.shake_time.store(shake - 1, Ordering::Relaxed); + } + + if self.in_ground.load(Ordering::Relaxed) { + let _in_ground_time = self.in_ground_time.fetch_add(1, Ordering::Relaxed); + let life = self.life.fetch_add(1, Ordering::Relaxed); + + // Despawn after enough time + if life >= Self::DESPAWN_TIME { + entity.remove().await; + } + return; + } + + // Trident is flying + let start_pos = entity.pos.load(); + let mut velocity = entity.velocity.load(); + + // Apply gravity + velocity.y -= Self::GRAVITY; + + // Apply inertia (air resistance or water drag) + let inertia = if entity.touching_water.load(Ordering::Relaxed) { + Self::WATER_INERTIA + } else { + Self::AIR_INERTIA + }; + velocity = velocity.multiply(inertia, inertia, inertia); + + entity.velocity.store(velocity); + + // Update rotation based on velocity + let len = velocity.horizontal_length(); + entity.set_rotation( + velocity.x.atan2(velocity.z) as f32 * 57.295_776, + velocity.y.atan2(len) as f32 * 57.295_776, + ); + + // Move trident + let new_pos = start_pos.add(&velocity); + entity.set_pos(new_pos); + + // Broadcast velocity update + let packet = CEntityVelocity::new(entity.entity_id.into(), velocity); + let chunk_pos = entity.chunk_pos.load(); + world.broadcast_to_chunk(chunk_pos, &packet); + + // Check for collisions using raycasting + let search_box = BoundingBox::new( + Vector3::new( + start_pos.x.min(new_pos.x), + start_pos.y.min(new_pos.y), + start_pos.z.min(new_pos.z), + ), + Vector3::new( + start_pos.x.max(new_pos.x), + start_pos.y.max(new_pos.y), + start_pos.z.max(new_pos.z), + ), + ) + .expand(0.3, 0.3, 0.3); + + let mut closest_t = 1.0f64; + let mut hit = None; + + // Block collisions + let (block_cols, block_positions) = world + .get_block_collisions(search_box, self.get_entity()) + .await; + for (idx, bb) in block_cols.iter().enumerate() { + if let Some(t) = calculate_ray_intersection(&start_pos, &velocity, bb) + && t < closest_t + { + closest_t = t; + + // Map back to block pos + let mut curr = 0; + for (len, pos) in &block_positions { + curr += len; + if idx < curr { + let hit_pos = start_pos.add(&velocity.multiply(t, t, t)); + hit = Some(ProjectileHit::Block { + pos: *pos, + face: get_hit_face(hit_pos, *pos), + hit_pos, + normal: velocity.normalize().multiply(-1.0, -1.0, -1.0), + }); + break; + } + } + } + } + + // Entity collisions + let candidates = world.get_entities_at_box(&search_box); + for cand in candidates { + if self.should_skip_collision(entity, &cand) { + continue; + } + + let ebb = cand.get_entity().bounding_box.load().expand(0.3, 0.3, 0.3); + if let Some(t) = calculate_ray_intersection(&start_pos, &velocity, &ebb) + && t < closest_t + { + closest_t = t; + let hit_pos = start_pos.add(&velocity.multiply(t, t, t)); + hit = Some(ProjectileHit::Entity { + entity: cand.clone(), + hit_pos, + normal: velocity.normalize().multiply(-1.0, -1.0, -1.0), + }); + } + } + + // Handle hit + if let Some(h) = hit { + if !self.has_hit.swap(true, Ordering::SeqCst) { + caller.on_hit(h).await; + } + } + }) + } + + fn get_entity(&self) -> &Entity { + &self.entity + } + + fn get_living_entity(&self) -> Option<&LivingEntity> { + None + } + + fn as_nbt_storage(&self) -> &dyn NBTStorage { + self + } + + fn cast_any(&self) -> &dyn std::any::Any { + self + } + + fn on_hit(&self, hit: ProjectileHit) -> EntityBaseFuture<'_, ()> { + Box::pin(async move { + let entity = self.get_entity(); + let world = entity.world.load(); + + match hit { + ProjectileHit::Block { pos, hit_pos, .. } => { + self.in_ground.store(true, Ordering::Relaxed); + self.shake_time.store(7, Ordering::Relaxed); + *self.last_block_pos.write().unwrap() = Some(pos); + + // Stop the trident + entity.velocity.store(Vector3::new(0.0, 0.0, 0.0)); + entity.set_pos(hit_pos); + + // Play sound + let sound_packet = CSoundEffect::new( + IdOr::Id(Sound::ItemTridentHitGround as u16), + SoundCategory::Neutral, + &hit_pos, + 1.0, + 1.0, + 0.0, + ); + let chunk_pos = entity.chunk_pos.load(); + world.broadcast_to_chunk(chunk_pos, &sound_packet); + } + ProjectileHit::Entity { + entity: target, + hit_pos, + .. + } => { + let mut damage = Self::BASE_DAMAGE; + + // Apply Impaling enchantment extra damage + if let Some(enchantments) = self + .item_stack + .lock() + .await + .get_data_component::( + ) { + for (enchantment, level) in enchantments.enchantment.iter() { + if **enchantment == pumpkin_data::Enchantment::IMPALING { + let in_water = + target.get_entity().touching_water.load(Ordering::Relaxed); + if in_water { + damage += 1.25 * f64::from(*level); + } + } + } + } + + target + .damage(&*target, damage as f32, DamageType::TRIDENT) + .await; + + // Play hit sound + let sound_packet = CSoundEffect::new( + IdOr::Id(Sound::ItemTridentHit as u16), + SoundCategory::Neutral, + &hit_pos, + 1.0, + 1.0, + 0.0, + ); + world.broadcast_packet_all(&sound_packet); + + // Standard bounce/fall-back behavior + entity.velocity.store(Vector3::new(0.0, -0.1, 0.0)); + self.has_hit.store(false, Ordering::Relaxed); // Let it hit the ground + } + } + }) + } + + fn on_player_collision<'a>(&'a self, player: &'a Arc) -> EntityBaseFuture<'a, ()> { + Box::pin(async move { + // Can only pick up when on the ground + if !self.in_ground.load(Ordering::Relaxed) { + return; + } + + if player.living_entity.health.load() <= 0.0 { + return; + } + + match self.pickup { + ArrowPickup::Disallowed => return, + ArrowPickup::CreativeOnly if !player.is_creative() => return, + _ => {} + } + + let mut stack = self.item_stack.lock().await.clone(); + if player.is_creative() || player.inventory.insert_stack_anywhere(&mut stack).await { + player.living_entity.pickup(&self.entity, 1); + self.get_entity().remove().await; + } + }) + } +} + +/// Ray intersection algorithm for AABBs, returning a t value +fn calculate_ray_intersection( + start: &Vector3, + dir: &Vector3, + bb: &BoundingBox, +) -> Option { + let mut t_min = 0.0f64; + let mut t_max = 1.0f64; + + let b_min = [bb.min.x, bb.min.y, bb.min.z]; + let b_max = [bb.max.x, bb.max.y, bb.max.z]; + let s = [start.x, start.y, start.z]; + let d = [dir.x, dir.y, dir.z]; + + for i in 0..3 { + if d[i].abs() < 1e-9 { + if s[i] < b_min[i] || s[i] > b_max[i] { + return None; + } + } else { + let t1 = (b_min[i] - s[i]) / d[i]; + let t2 = (b_max[i] - s[i]) / d[i]; + t_min = t_min.max(t1.min(t2)); + t_max = t_max.min(t1.max(t2)); + } + } + + (0.0..=1.0).contains(&t_min).then_some(t_min) +} + +/// Get the face of the block that was hit +fn get_hit_face(hit_pos: Vector3, block_pos: BlockPos) -> pumpkin_data::BlockDirection { + let local = hit_pos.sub(&block_pos.0.to_f64()); + let eps = 1.0e-4; + + if local.x <= eps { + pumpkin_data::BlockDirection::West + } else if local.x >= 1.0 - eps { + pumpkin_data::BlockDirection::East + } else if local.y <= eps { + pumpkin_data::BlockDirection::Down + } else if local.y >= 1.0 - eps { + pumpkin_data::BlockDirection::Up + } else if local.z <= eps { + pumpkin_data::BlockDirection::North + } else { + pumpkin_data::BlockDirection::South + } +} diff --git a/pumpkin/src/entity/type.rs b/pumpkin/src/entity/type.rs index 20120f62c..2fef6c4f5 100644 --- a/pumpkin/src/entity/type.rs +++ b/pumpkin/src/entity/type.rs @@ -105,6 +105,7 @@ use crate::entity::projectile::shulker_bullet::ShulkerBulletEntity; use crate::entity::projectile::small_fireball::SmallFireballEntity; use crate::entity::projectile::snowball::SnowballEntity; use crate::entity::projectile::splash_potion::SplashPotionEntity; +use crate::entity::projectile::trident::TridentEntity; use crate::entity::projectile::wind_charge::{WIND_CHARGE_GRAVITY, WindChargeEntity}; use crate::entity::tnt::TNTEntity; use crate::entity::vehicle::boat::BoatEntity; @@ -238,6 +239,7 @@ pub fn from_type( id if id == EntityType::ITEM.id => Arc::new(ItemEntity::new_for_restore(entity)), id if id == EntityType::ARROW.id => Arc::new(ArrowEntity::new(entity, None)), id if id == EntityType::SPECTRAL_ARROW.id => Arc::new(ArrowEntity::new(entity, None)), + id if id == EntityType::TRIDENT.id => Arc::new(TridentEntity::new(entity, None)), id if id == EntityType::FIREBALL.id => Arc::new(FireballEntity::new(entity)), id if id == EntityType::SMALL_FIREBALL.id => Arc::new(SmallFireballEntity::new(entity)), id if id == EntityType::WIND_CHARGE.id => { diff --git a/pumpkin/src/item/items/bow.rs b/pumpkin/src/item/items/bow.rs index e21ba162f..ff2bd1864 100644 --- a/pumpkin/src/item/items/bow.rs +++ b/pumpkin/src/item/items/bow.rs @@ -98,11 +98,27 @@ impl BowItem { // Calculate power and fire let power = Self::get_power_for_time(use_ticks); + + // Check for Infinity enchantment + let mut has_infinity = false; + let held = player.inventory().held_item(); + if let Some(enchantments) = + held.lock() + .await + .get_data_component::() + { + has_infinity = enchantments + .enchantment + .iter() + .any(|(e, _)| **e == pumpkin_data::Enchantment::INFINITY); + } + Self.fire_arrow(player, power).await; - // Consume arrow (if not creative) + // Consume arrow (if not creative and no Infinity) if let Some(slot) = arrow_slot && gamemode != GameMode::Creative + && !has_infinity { player.consume_arrow(slot).await; } @@ -147,7 +163,26 @@ impl BowItem { ArrowPickup::Allowed }; - let arrow = ArrowEntity::new_shot(arrow_entity, player.get_entity(), pickup); + let mut arrow = ArrowEntity::new_shot(arrow_entity, player.get_entity(), pickup); + + // Read enchantments of the held item (bow) + let held = player.inventory().held_item(); + let stack = held.lock().await; + if let Some(enchantments) = + stack.get_data_component::() + { + for (enchantment, level) in enchantments.enchantment.iter() { + if **enchantment == pumpkin_data::Enchantment::POWER { + arrow.base_damage = + arrow.base_damage * (1.0 + 0.25 * (f64::from(*level) + 1.0)); + } else if **enchantment == pumpkin_data::Enchantment::PUNCH { + arrow.punch_level.store(*level as u8, Ordering::Relaxed); + } else if **enchantment == pumpkin_data::Enchantment::FLAME { + arrow.is_flame.store(true, Ordering::Relaxed); + } + } + } + drop(stack); // Set velocity based on player's look direction and power let (yaw, pitch) = player.rotation(); diff --git a/pumpkin/src/item/items/trident.rs b/pumpkin/src/item/items/trident.rs index 1a8df5fb8..5354fcafa 100644 --- a/pumpkin/src/item/items/trident.rs +++ b/pumpkin/src/item/items/trident.rs @@ -1,10 +1,20 @@ -use pumpkin_data::item::Item; -use pumpkin_util::GameMode; +use std::any::Any; +use std::future::Future; +use std::pin::Pin; +use std::sync::Arc; -use crate::{ - entity::player::Player, - item::{ItemBehaviour, ItemMetadata}, -}; +use crate::entity::player::Player; +use crate::entity::projectile::arrow::ArrowPickup; +use crate::entity::projectile::trident::TridentEntity; +use crate::entity::{Entity, EntityBase}; +use crate::item::{ItemBehaviour, ItemMetadata}; +use pumpkin_data::entity::EntityType; +use pumpkin_data::item::Item; +use pumpkin_data::item_stack::ItemStack; +use pumpkin_data::sound::{Sound, SoundCategory}; +use pumpkin_util::GameMode; +use pumpkin_util::math::vector3::Vector3; +use pumpkin_world::inventory::Inventory; pub struct TridentItem; @@ -15,11 +25,149 @@ impl ItemMetadata for TridentItem { } impl ItemBehaviour for TridentItem { + fn normal_use<'a>( + &'a self, + _item: &'a Item, + player: &'a Player, + ) -> Pin + Send + 'a>> { + Box::pin(async move { + let inventory = player.inventory(); + let held = inventory.held_item(); + let stack = held.lock().await.clone(); + + player + .living_entity + .set_active_hand(pumpkin_util::Hand::Right, stack, 72000) + .await; + }) + } + + fn on_stopped_using<'a>( + &'a self, + _stack: &'a ItemStack, + player: &'a Player, + ) -> Pin + Send + 'a>> { + Box::pin(async move { + let use_ticks = player + .living_entity + .item_use_time + .load(std::sync::atomic::Ordering::Relaxed); + let use_ticks = 72000 - use_ticks; + + if use_ticks < 10 { + return; + } + + let world = player.world(); + let held = player.inventory().held_item(); + let stack_guard = held.lock().await.clone(); + + // Check Riptide level + let mut riptide_level = 0u32; + if let Some(enchantments) = stack_guard + .get_data_component::( + ) { + for (enchantment, level) in enchantments.enchantment.iter() { + if **enchantment == pumpkin_data::Enchantment::RIPTIDE { + riptide_level = *level as u32; + } + } + } + + if riptide_level > 0 { + let is_touching_water = player + .living_entity + .entity + .touching_water + .load(std::sync::atomic::Ordering::Relaxed); + let is_raining = world.is_raining().await; + + if is_touching_water || is_raining { + let (yaw, pitch) = player.rotation(); + let look_vec = Vector3::rotation_vector(pitch as f64, yaw as f64); + let speed = f64::from(riptide_level).mul_add(0.75, 1.5); + let launch_velocity = look_vec.multiply(speed, speed, speed); + + player.get_entity().set_velocity(launch_velocity); + player.get_entity().send_velocity(); + + let sound = match riptide_level { + 1 => Sound::ItemTridentRiptide1, + 2 => Sound::ItemTridentRiptide2, + _ => Sound::ItemTridentRiptide3, + }; + world.play_sound(sound, SoundCategory::Players, &player.position()); + + player.living_entity.clear_active_hand().await; + + if player.gamemode.load() != GameMode::Creative { + player.damage_held_item(1).await; + } + return; + } + } + + // Normal throw + let entity = Entity::new(world.clone(), player.position(), &EntityType::TRIDENT); + let pickup = if player.gamemode.load() == GameMode::Creative { + ArrowPickup::CreativeOnly + } else { + ArrowPickup::Allowed + }; + + let trident_entity = + TridentEntity::new_shot(entity, player.get_entity(), stack_guard, pickup); + + let (yaw, pitch) = player.rotation(); + trident_entity.set_velocity_from_rotation(pitch, yaw, 0.0, 2.5, 1.0); + + let trident_arc: Arc = Arc::new(trident_entity); + world.spawn_entity(trident_arc).await; + + world.play_sound( + Sound::ItemTridentThrow, + SoundCategory::Players, + &player.position(), + ); + + if player.gamemode.load() != GameMode::Creative { + let inventory = player.inventory(); + let selected_slot = inventory.get_selected_slot() as usize; + + let main_hand_item = inventory.get_stack(selected_slot).await; + let mut stack_lock = main_hand_item.lock().await; + if stack_lock.item.id == Item::TRIDENT.id { + *stack_lock = ItemStack::EMPTY.clone(); + player + .sync_hand_slot(selected_slot, ItemStack::EMPTY.clone()) + .await; + } else { + let off_hand_slot = + pumpkin_inventory::player::player_inventory::PlayerInventory::OFF_HAND_SLOT; + let off_hand_item = inventory.get_stack(off_hand_slot).await; + let mut off_stack_lock = off_hand_item.lock().await; + if off_stack_lock.item.id == Item::TRIDENT.id { + *off_stack_lock = ItemStack::EMPTY.clone(); + player + .sync_hand_slot(off_hand_slot, ItemStack::EMPTY.clone()) + .await; + } + } + } + + player.living_entity.clear_active_hand().await; + }) + } + fn can_mine(&self, player: &Player) -> bool { player.gamemode.load() != GameMode::Creative } - fn as_any(&self) -> &dyn std::any::Any { + fn get_use_duration(&self) -> i32 { + 72000 + } + + fn as_any(&self) -> &dyn Any { self } }