From 0b2ed1af38ff727f21caf7b1e45c4e456e97581c Mon Sep 17 00:00:00 2001 From: "Eshan I." <2027eiyer@tjhsst.edu> Date: Sun, 2 Aug 2026 14:08:10 -0400 Subject: [PATCH] fix(projectile): preserve arrow payloads (#2710) --- pumpkin-codegen/src/item.rs | 7 +- pumpkin-data/src/data_component_impl/food.rs | 52 +++- pumpkin-data/src/data_component_impl/mod.rs | 3 + pumpkin-data/src/generated/item.rs | 10 +- .../src/block/blocks/redstone/dispenser.rs | 18 +- pumpkin/src/entity/player.rs | 16 +- pumpkin/src/entity/projectile/arrow.rs | 269 ++++++++++++++++-- pumpkin/src/item/items/arrow.rs | 6 +- pumpkin/src/item/items/bow.rs | 25 +- pumpkin/src/item/items/crossbow.rs | 41 ++- pumpkin/src/item/potion.rs | 54 +++- 11 files changed, 422 insertions(+), 79 deletions(-) diff --git a/pumpkin-codegen/src/item.rs b/pumpkin-codegen/src/item.rs index 18178e513..62c98013a 100644 --- a/pumpkin-codegen/src/item.rs +++ b/pumpkin-codegen/src/item.rs @@ -133,7 +133,7 @@ pub struct ItemComponents { #[serde(rename = "minecraft:potion_contents")] pub potion_contents: Option, #[serde(rename = "minecraft:potion_duration_scale")] - pub potion_duration_scale: Option, + pub potion_duration_scale: Option, #[serde(rename = "minecraft:provides_banner_patterns")] pub provides_banner_patterns: Option, #[serde(rename = "minecraft:provides_trim_material")] @@ -833,8 +833,9 @@ impl ToTokens for ItemComponents { }), }); } - if self.potion_duration_scale.is_some() { - tokens.extend(quote! { (PotionDurationScale, &PotionDurationScaleImpl), }); + if let Some(scale) = self.potion_duration_scale { + let scale_lit = LitFloat::new(&format!("{scale:?}f32"), Span::call_site()); + tokens.extend(quote! { (PotionDurationScale, &PotionDurationScaleImpl { scale: #scale_lit }), }); } if self.provides_banner_patterns.is_some() { tokens.extend(quote! { (ProvidesBannerPatterns, &ProvidesBannerPatternsImpl), }); diff --git a/pumpkin-data/src/data_component_impl/food.rs b/pumpkin-data/src/data_component_impl/food.rs index 5d2f0056b..1999eb956 100644 --- a/pumpkin-data/src/data_component_impl/food.rs +++ b/pumpkin-data/src/data_component_impl/food.rs @@ -535,11 +535,59 @@ impl DataComponentImpl for PotionContentsImpl { default_impl!(PotionContents); } -#[derive(Clone, Debug, Hash, PartialEq, Eq)] -pub struct PotionDurationScaleImpl; +#[derive(Clone, Debug, PartialEq)] +pub struct PotionDurationScaleImpl { + pub scale: f32, +} +impl PotionDurationScaleImpl { + pub fn read_data(data: &NbtTag) -> Option { + data.extract_float().map(|scale| Self { scale }) + } +} impl DataComponentImpl for PotionDurationScaleImpl { + fn write_data(&self) -> NbtTag { + NbtTag::Float(self.scale) + } + fn get_hash(&self) -> i32 { + get_f32_hash(self.scale) as i32 + } default_impl!(PotionDurationScale); } +impl Hash for PotionDurationScaleImpl { + fn hash(&self, state: &mut H) { + self.scale.to_bits().hash(state); + } +} + +#[cfg(test)] +mod tests { + use super::{DataComponentImpl, PotionDurationScaleImpl}; + use crate::item::Item; + + #[test] + fn potion_duration_scale_round_trips_as_a_float() { + let scale = PotionDurationScaleImpl { scale: 0.125 }; + let encoded = scale.write_data(); + let decoded = PotionDurationScaleImpl::read_data(&encoded).expect("scale should decode"); + + assert_eq!(decoded, scale); + } + + #[test] + fn generated_arrow_duration_scale_is_data_driven() { + let scale = Item::TIPPED_ARROW + .components + .iter() + .find_map(|(id, component)| { + (*id == crate::data_component::DataComponent::PotionDurationScale) + .then(|| component.as_any().downcast_ref::()) + .flatten() + }) + .expect("tipped arrows should have a duration scale"); + + assert_eq!(scale.scale, 0.125); + } +} #[derive(Clone, Debug, Hash, PartialEq, Eq)] pub struct SuspiciousStewEffectsImpl; diff --git a/pumpkin-data/src/data_component_impl/mod.rs b/pumpkin-data/src/data_component_impl/mod.rs index ed109c8ce..4c476beea 100644 --- a/pumpkin-data/src/data_component_impl/mod.rs +++ b/pumpkin-data/src/data_component_impl/mod.rs @@ -515,6 +515,9 @@ pub fn read_data(id: DataComponent, data: &NbtTag) -> Option Some(UnbreakableImpl::read_data(data)?.to_dyn()), DataComponent::DamageResistant => Some(DamageResistantImpl::read_data(data)?.to_dyn()), DataComponent::PotionContents => Some(PotionContentsImpl::read_data(data)?.to_dyn()), + DataComponent::PotionDurationScale => { + Some(PotionDurationScaleImpl::read_data(data)?.to_dyn()) + } DataComponent::Fireworks => Some(FireworksImpl::read_data(data)?.to_dyn()), DataComponent::FireworkExplosion => Some(FireworkExplosionImpl::read_data(data)?.to_dyn()), DataComponent::CustomName => Some(CustomNameImpl::read_data(data)?.to_dyn()), diff --git a/pumpkin-data/src/generated/item.rs b/pumpkin-data/src/generated/item.rs index 82bc72e94..cee29a369 100644 --- a/pumpkin-data/src/generated/item.rs +++ b/pumpkin-data/src/generated/item.rs @@ -32610,7 +32610,10 @@ impl Item { custom_name: None, }, ), - (PotionDurationScale, &PotionDurationScaleImpl), + ( + PotionDurationScale, + &PotionDurationScaleImpl { scale: 0.25f32 }, + ), (Rarity, &RarityImpl), (RepairCost, &RepairCostImpl), (SwingAnimation, &SwingAnimationImpl), @@ -56145,7 +56148,10 @@ impl Item { custom_name: None, }, ), - (PotionDurationScale, &PotionDurationScaleImpl), + ( + PotionDurationScale, + &PotionDurationScaleImpl { scale: 0.125f32 }, + ), (Rarity, &RarityImpl), (RepairCost, &RepairCostImpl), (SwingAnimation, &SwingAnimationImpl), diff --git a/pumpkin/src/block/blocks/redstone/dispenser.rs b/pumpkin/src/block/blocks/redstone/dispenser.rs index 05d172582..08d7ef704 100644 --- a/pumpkin/src/block/blocks/redstone/dispenser.rs +++ b/pumpkin/src/block/blocks/redstone/dispenser.rs @@ -259,21 +259,19 @@ impl DispenserBlock { const ARROW_DISPENSE_UNCERTAINTY: f64 = 6.0; async fn fire_arrow(ctx: &DispenseContext<'_>, item: &mut ItemStack) { - // TODO: Add tipped arrows - let entity_type = if item.item.id == Item::SPECTRAL_ARROW.id { - &EntityType::SPECTRAL_ARROW - } else { - &EntityType::ARROW - }; - let _ = item.split(1); + let projectile = item.split(1); let facing = to_normal(ctx.facing); let position = ctx.position.to_centered_f64().add(&(facing * 0.7)); let world = ctx.world; - let arrow_entity = Entity::new(world.clone(), position, entity_type); - let mut arrow = ArrowEntity::new(arrow_entity, None); - arrow.pickup = ArrowPickup::Allowed; + let arrow_entity = Entity::new( + world.clone(), + position, + ArrowEntity::entity_type_for_item(projectile.item), + ); + let arrow = + ArrowEntity::new_with_item(arrow_entity, None, &projectile, ArrowPickup::Allowed); arrow.set_velocity( facing.x, diff --git a/pumpkin/src/entity/player.rs b/pumpkin/src/entity/player.rs index d12679803..94f7e9755 100644 --- a/pumpkin/src/entity/player.rs +++ b/pumpkin/src/entity/player.rs @@ -4104,7 +4104,13 @@ impl Player { // Check offhand first let stack = inventory.get_stack(PlayerInventory::OFF_HAND_SLOT).await; let item = stack.lock().await; - if item.item.id == Item::ARROW.id && item.item_count > 0 { + if matches!( + item.item.id, + id if id == Item::ARROW.id + || id == Item::TIPPED_ARROW.id + || id == Item::SPECTRAL_ARROW.id + ) && item.item_count > 0 + { return Some(PlayerInventory::OFF_HAND_SLOT); } drop(item); @@ -4113,7 +4119,13 @@ impl Player { for slot in 0..PlayerInventory::MAIN_SIZE { let stack = inventory.get_stack(slot).await; let item = stack.lock().await; - if item.item.id == Item::ARROW.id && item.item_count > 0 { + if matches!( + item.item.id, + id if id == Item::ARROW.id + || id == Item::TIPPED_ARROW.id + || id == Item::SPECTRAL_ARROW.id + ) && item.item_count > 0 + { return Some(slot); } } diff --git a/pumpkin/src/entity/projectile/arrow.rs b/pumpkin/src/entity/projectile/arrow.rs index 85c5716b7..0f9fbf8ed 100644 --- a/pumpkin/src/entity/projectile/arrow.rs +++ b/pumpkin/src/entity/projectile/arrow.rs @@ -1,14 +1,18 @@ use std::sync::Arc; use std::sync::atomic::{AtomicBool, AtomicU8, AtomicU32, Ordering}; +use tokio::sync::RwLock; use crate::entity::projectile::ProjectileHit; use crate::{ entity::{ - Entity, EntityBase, EntityBaseFuture, NBTStorage, living::LivingEntity, player::Player, + Entity, EntityBase, EntityBaseFuture, NBTStorage, NbtFuture, living::LivingEntity, + player::Player, }, server::Server, }; use pumpkin_data::damage::DamageType; +use pumpkin_data::data_component_impl::PotionDurationScaleImpl; +use pumpkin_data::entity::EntityType; use pumpkin_data::item::Item; use pumpkin_data::item_stack::ItemStack; use pumpkin_data::particle::Particle; @@ -51,6 +55,7 @@ impl ArrowPickup { pub struct ArrowEntity { pub entity: Entity, pub owner_id: Option, + pub item_stack: RwLock, pub base_damage: f64, pub pickup: ArrowPickup, pub is_critical: AtomicBool, @@ -73,33 +78,20 @@ impl ArrowEntity { const DESPAWN_TIME: u32 = 1200; pub fn new(entity: Entity, owner_id: Option) -> Self { + let item_stack = ItemStack::new(1, Self::default_item(entity.entity_type)); + Self::new_with_item(entity, owner_id, &item_stack, ArrowPickup::Disallowed) + } + + pub fn new_with_item( + entity: Entity, + owner_id: Option, + item_stack: &ItemStack, + pickup: ArrowPickup, + ) -> Self { Self { entity, owner_id, - base_damage: Self::ARROW_BASE_DAMAGE, - 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), - 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, 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: RwLock::new(item_stack.copy_with_count(1)), base_damage: Self::ARROW_BASE_DAMAGE, pickup, is_critical: AtomicBool::new(false), @@ -115,6 +107,86 @@ impl ArrowEntity { } } + 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: RwLock::new(item_stack.copy_with_count(1)), + base_damage: Self::ARROW_BASE_DAMAGE, + 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), + shake_time: AtomicU8::new(0), + has_hit: AtomicBool::new(false), + last_block_pos: Arc::new(std::sync::RwLock::new(None)), + } + } + + #[must_use] + pub const fn entity_type_for_item(item: &'static Item) -> &'static EntityType { + if item.id == Item::SPECTRAL_ARROW.id { + &EntityType::SPECTRAL_ARROW + } else { + &EntityType::ARROW + } + } + + #[must_use] + pub const fn default_item(entity_type: &'static EntityType) -> &'static Item { + if entity_type.id == EntityType::SPECTRAL_ARROW.id { + &Item::SPECTRAL_ARROW + } else { + &Item::ARROW + } + } + + fn write_item_stack_nbt(item_stack: &ItemStack, nbt: &mut pumpkin_nbt::compound::NbtCompound) { + let mut item = pumpkin_nbt::compound::NbtCompound::new(); + item_stack.copy_with_count(1).write_item_stack(&mut item); + nbt.put_compound("item", item); + } + + fn read_item_stack_nbt(nbt: &pumpkin_nbt::compound::NbtCompound) -> Option { + nbt.get_compound("item") + .and_then(ItemStack::read_item_stack) + .map(|item_stack| item_stack.copy_with_count(1)) + } + + fn pickup_item_stack(item_stack: &ItemStack) -> ItemStack { + item_stack.copy_with_count(1) + } + + const fn spectral_glowing_effect() -> pumpkin_data::potion::Effect { + pumpkin_data::potion::Effect { + effect_type: &pumpkin_data::effect::StatusEffect::GLOWING, + duration: 200, + amplifier: 0, + ambient: false, + show_particles: true, + show_icon: true, + blend: false, + } + } + + const fn should_apply_post_hurt_effects(damage_succeeded: bool) -> bool { + damage_succeeded + } + pub fn set_velocity_from_rotation( &self, pitch: f32, @@ -190,7 +262,30 @@ impl ArrowEntity { } } -impl NBTStorage for ArrowEntity {} +impl NBTStorage for ArrowEntity { + fn write_nbt<'a>( + &'a self, + nbt: &'a mut pumpkin_nbt::compound::NbtCompound, + ) -> NbtFuture<'a, ()> { + Box::pin(async move { + self.entity.write_nbt(nbt).await; + let item_stack = self.item_stack.read().await; + Self::write_item_stack_nbt(&item_stack, nbt); + }) + } + + fn read_nbt_non_mut<'a>( + &'a self, + nbt: &'a pumpkin_nbt::compound::NbtCompound, + ) -> NbtFuture<'a, ()> { + Box::pin(async move { + self.entity.read_nbt_non_mut(nbt).await; + if let Some(item_stack) = Self::read_item_stack_nbt(nbt) { + *self.item_stack.write().await = item_stack; + } + }) + } +} impl EntityBase for ArrowEntity { #[allow(clippy::too_many_lines)] @@ -408,11 +503,11 @@ impl EntityBase for ArrowEntity { target.get_entity().set_on_fire_for_ticks(100); } - target + let damage_succeeded = target .damage(&*target, damage as f32, DamageType::ARROW) .await; - if target.get_living_entity().is_some() { + if let Some(living) = target.get_living_entity() { let punch = self.punch_level.load(Ordering::Relaxed); if punch > 0 && let Some(owner_id) = self.owner_id @@ -435,6 +530,26 @@ impl EntityBase for ArrowEntity { 0.0, ); world.broadcast_packet_all(&sound_packet); + + if Self::should_apply_post_hurt_effects(damage_succeeded) { + let item_stack = self.item_stack.read().await.clone(); + let scale = item_stack + .get_data_component::() + .map_or(1.0, |component| component.scale); + crate::item::potion::PotionContents::apply_effects_to( + living, + crate::item::potion::PotionContents::read_potion_effects( + &item_stack, + ), + scale, + crate::item::potion::PotionApplicationSource::Arrow, + ) + .await; + + if entity.entity_type.id == EntityType::SPECTRAL_ARROW.id { + living.add_effect(Self::spectral_glowing_effect()).await; + } + } } // Check pierce level @@ -482,7 +597,8 @@ impl EntityBase for ArrowEntity { } // Try to insert an arrow into the player's inventory - let mut stack = ItemStack::new(1, &Item::ARROW); + let item_stack = self.item_stack.read().await; + let mut stack = Self::pickup_item_stack(&item_stack); if player.is_creative() || player.inventory.insert_stack_anywhere(&mut stack).await { player.living_entity.pickup(&self.entity, 1); @@ -512,7 +628,8 @@ impl ArrowEntity { } // Skip other arrows, item entities, and falling block entities - if other_ent.entity_type == &pumpkin_data::entity::EntityType::ARROW + if (other_ent.entity_type == &pumpkin_data::entity::EntityType::ARROW + || other_ent.entity_type == &pumpkin_data::entity::EntityType::SPECTRAL_ARROW) || other_ent.entity_type == &pumpkin_data::entity::EntityType::ITEM || other_ent.entity_type == &pumpkin_data::entity::EntityType::FALLING_BLOCK { @@ -574,3 +691,95 @@ fn get_hit_face(hit_pos: Vector3, block_pos: BlockPos) -> pumpkin_data::Blo BlockDirection::South } } + +#[cfg(test)] +mod tests { + use super::ArrowEntity; + use pumpkin_data::data_component::DataComponent; + use pumpkin_data::data_component_impl::{ + DataComponentImpl, PotionContentsImpl, PotionDurationScaleImpl, + }; + use pumpkin_data::entity::EntityType; + use pumpkin_data::item::Item; + use pumpkin_data::item_stack::ItemStack; + + fn tipped_payload(count: u8) -> ItemStack { + let mut tipped = ItemStack::new(32, &Item::TIPPED_ARROW); + tipped.patch.push(( + DataComponent::PotionContents, + Some( + PotionContentsImpl { + potion_id: Some(5), + custom_color: Some(0x123456), + custom_effects: Vec::new(), + custom_name: Some("payload".to_string()), + } + .to_dyn(), + ), + )); + tipped.patch.push(( + DataComponent::PotionDurationScale, + Some(PotionDurationScaleImpl { scale: 0.5 }.to_dyn()), + )); + tipped.copy_with_count(count) + } + + #[test] + fn projectile_payload_keeps_components_at_one_count() { + let tipped = tipped_payload(32); + + let payload = tipped.copy_with_count(1); + + assert_eq!(payload.item_count, 1); + assert!(payload.are_items_and_components_equal(&tipped)); + assert_eq!( + ArrowEntity::entity_type_for_item(payload.item), + &EntityType::ARROW + ); + assert_eq!( + ArrowEntity::entity_type_for_item(&Item::SPECTRAL_ARROW), + &EntityType::SPECTRAL_ARROW + ); + } + + #[test] + fn arrow_nbt_payload_round_trips() { + let payload = tipped_payload(1); + let mut nbt = pumpkin_nbt::compound::NbtCompound::new(); + + ArrowEntity::write_item_stack_nbt(&payload, &mut nbt); + let restored = ArrowEntity::read_item_stack_nbt(&nbt).expect("arrow payload should decode"); + + assert!(restored.are_equal(&payload)); + assert_eq!(restored.item_count, 1); + } + + #[test] + fn grounded_pickup_stack_keeps_exact_arrow_payload() { + let payload = tipped_payload(32); + let pickup = ArrowEntity::pickup_item_stack(&payload); + + assert_eq!(pickup.item_count, 1); + assert!(pickup.are_items_and_components_equal(&payload)); + } + + #[test] + fn spectral_arrow_applies_vanilla_glowing_effect() { + let effect = ArrowEntity::spectral_glowing_effect(); + + assert_eq!( + effect.effect_type, + &pumpkin_data::effect::StatusEffect::GLOWING + ); + assert_eq!(effect.duration, 200); + assert_eq!(effect.amplifier, 0); + assert!(effect.show_particles); + assert!(effect.show_icon); + } + + #[test] + fn post_hurt_effects_require_successful_arrow_damage() { + assert!(!ArrowEntity::should_apply_post_hurt_effects(false)); + assert!(ArrowEntity::should_apply_post_hurt_effects(true)); + } +} diff --git a/pumpkin/src/item/items/arrow.rs b/pumpkin/src/item/items/arrow.rs index eb939e4c8..c8f2c1325 100644 --- a/pumpkin/src/item/items/arrow.rs +++ b/pumpkin/src/item/items/arrow.rs @@ -7,7 +7,11 @@ pub struct ArrowItem; impl ItemMetadata for ArrowItem { fn ids() -> Box<[u16]> { - Box::new([Item::ARROW.id]) + Box::new([ + Item::ARROW.id, + Item::TIPPED_ARROW.id, + Item::SPECTRAL_ARROW.id, + ]) } } diff --git a/pumpkin/src/item/items/bow.rs b/pumpkin/src/item/items/bow.rs index 7fa7f17d6..f8da60500 100644 --- a/pumpkin/src/item/items/bow.rs +++ b/pumpkin/src/item/items/bow.rs @@ -8,13 +8,13 @@ use crate::entity::player::Player; use crate::entity::projectile::arrow::{ArrowEntity, ArrowPickup}; 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_protocol::IdOr; use pumpkin_protocol::java::client::play::CSoundEffect; use pumpkin_util::GameMode; +use pumpkin_world::inventory::Inventory; pub struct BowItem; @@ -96,6 +96,14 @@ impl BowItem { return; } + let projectile = if let Some(slot) = arrow_slot { + let stack = player.inventory().get_stack(slot).await; + stack.lock().await.copy_with_count(1) + } else { + ItemStack::new(1, &Item::ARROW) + }; + let infinite_projectile = projectile.item.id == Item::ARROW.id; + // Calculate power and fire let power = Self::get_power_for_time(use_ticks); @@ -113,12 +121,12 @@ impl BowItem { .any(|(e, _)| **e == pumpkin_data::Enchantment::INFINITY); } - Self.fire_arrow(player, power).await; + Self::fire_arrow(player, power, projectile).await; // Consume arrow (if not creative and no Infinity) if let Some(slot) = arrow_slot && gamemode != GameMode::Creative - && !has_infinity + && !(has_infinity && infinite_projectile) { player.consume_arrow(slot).await; } @@ -144,7 +152,7 @@ impl BowItem { } /// Fire an arrow from the bow - pub async fn fire_arrow(&self, player: &Player, power: f32) { + pub async fn fire_arrow(player: &Player, power: f32, projectile: ItemStack) { if power < 0.1 { return; // Not enough charge } @@ -153,7 +161,11 @@ impl BowItem { let position = player.position(); // Create arrow entity - let arrow_entity = Entity::new(world.clone(), position, &EntityType::ARROW); + let arrow_entity = Entity::new( + world.clone(), + position, + ArrowEntity::entity_type_for_item(projectile.item), + ); // Determine pickup mode based on gamemode let gamemode = player.gamemode.load(); @@ -163,7 +175,8 @@ impl BowItem { ArrowPickup::Allowed }; - let mut arrow = ArrowEntity::new_shot(arrow_entity, player.get_entity(), pickup); + let mut arrow = + ArrowEntity::new_shot(arrow_entity, player.get_entity(), &projectile, pickup); // Read enchantments of the held item (bow) let held = player.inventory().held_item(); diff --git a/pumpkin/src/item/items/crossbow.rs b/pumpkin/src/item/items/crossbow.rs index b3c7d37b9..0f6d27bb4 100644 --- a/pumpkin/src/item/items/crossbow.rs +++ b/pumpkin/src/item/items/crossbow.rs @@ -11,7 +11,6 @@ use crate::entity::{Entity, EntityBase}; use crate::item::{ItemBehaviour, ItemMetadata}; use pumpkin_data::data_component::DataComponent; use pumpkin_data::data_component_impl::{ChargedProjectilesImpl, EnchantmentsImpl}; -use pumpkin_data::entity::EntityType; use pumpkin_data::item::Item; use pumpkin_data::item_stack::ItemStack; use pumpkin_data::sound::{Sound, SoundCategory}; @@ -90,7 +89,9 @@ impl ItemBehaviour for CrossbowItem { let arrow_stack_arc = inventory.get_stack(slot).await; let arrow_stack = arrow_stack_arc.lock().await; let mut arrow_nbt = pumpkin_nbt::compound::NbtCompound::new(); - arrow_stack.write_item_stack(&mut arrow_nbt); + arrow_stack + .copy_with_count(1) + .write_item_stack(&mut arrow_nbt); drop(arrow_stack); (Some(arrow_nbt), slot) } else if player.gamemode.load() == GameMode::Creative { @@ -138,12 +139,11 @@ impl ItemBehaviour for CrossbowItem { impl CrossbowItem { async fn fire_projectiles(player: &Player, held: &Arc>) { - let mut stack = held.lock().await; - let projectiles = stack - .get_data_component::() - .cloned(); - - if let Some(charged) = projectiles { + let (projectiles, has_multishot) = { + let stack = held.lock().await; + let projectiles = stack + .get_data_component::() + .cloned(); let has_multishot = stack .get_data_component::() @@ -153,7 +153,10 @@ impl CrossbowItem { .iter() .any(|(e, _)| **e == pumpkin_data::Enchantment::MULTISHOT) }); + (projectiles, has_multishot) + }; + if let Some(charged) = projectiles { let world = player.world(); world.play_sound( Sound::ItemCrossbowShoot, @@ -163,7 +166,10 @@ impl CrossbowItem { let (yaw, pitch) = player.rotation(); - for _ in charged.projectiles { + for projectile_nbt in charged.projectiles { + let Some(projectile) = ItemStack::read_item_stack(&projectile_nbt) else { + continue; + }; let yaws = if has_multishot { vec![yaw - 10.0, yaw, yaw + 10.0] } else { @@ -171,22 +177,31 @@ impl CrossbowItem { }; for t_yaw in yaws { - let arrow_entity = - Entity::new(world.clone(), player.position(), &EntityType::ARROW); + let arrow_entity = Entity::new( + world.clone(), + player.position(), + ArrowEntity::entity_type_for_item(projectile.item), + ); let pickup = if player.gamemode.load() == GameMode::Creative { ArrowPickup::CreativeOnly } else { ArrowPickup::Allowed }; - let arrow = ArrowEntity::new_shot(arrow_entity, player.get_entity(), pickup); + let arrow = ArrowEntity::new_shot( + arrow_entity, + player.get_entity(), + &projectile, + pickup, + ); arrow.set_velocity_from_rotation(pitch, t_yaw, 0.0, 3.15, 1.0); let arrow_arc: Arc = Arc::new(arrow); world.spawn_entity(arrow_arc).await; } } - stack + held.lock() + .await .patch .retain(|(id, _)| *id != DataComponent::ChargedProjectiles); player.damage_held_item(1).await; diff --git a/pumpkin/src/item/potion.rs b/pumpkin/src/item/potion.rs index 3df45b6df..bc717b772 100644 --- a/pumpkin/src/item/potion.rs +++ b/pumpkin/src/item/potion.rs @@ -13,6 +13,24 @@ pub enum PotionApplicationSource { Normal, /// `AreaEffectCloud` application (shorter durations and weaker instant potency) AreaEffectCloud, + Arrow, +} + +impl PotionApplicationSource { + const fn instant_scale(self, scale: f32) -> f32 { + match self { + Self::AreaEffectCloud => scale * 0.5, + Self::Arrow => 1.0, + Self::Normal => scale, + } + } + + const fn duration_scale(self, scale: f32) -> f32 { + match self { + Self::AreaEffectCloud => scale * 0.25, + Self::Arrow | Self::Normal => scale, + } + } } impl PotionContents { @@ -128,11 +146,7 @@ impl PotionContents { if is_instant { // Instant potency scaling - let instant_scale = if source == PotionApplicationSource::AreaEffectCloud { - scale * 0.5 - } else { - scale - }; + let instant_scale = source.instant_scale(scale); // Apply instant effects logic directly as they don't tick if effect_type.id == pumpkin_data::effect::StatusEffect::INSTANT_HEALTH.id { @@ -163,11 +177,7 @@ impl PotionContents { target.add_effect(eff).await; } else { // Duration scaling - let duration_scale = if source == PotionApplicationSource::AreaEffectCloud { - scale * 0.25 - } else { - scale - }; + let duration_scale = source.duration_scale(scale); let dur = ((duration as f32) * duration_scale).max(1.0) as i32; let eff = pumpkin_data::potion::Effect { @@ -184,3 +194,27 @@ impl PotionContents { } } } + +#[cfg(test)] +mod tests { + use super::PotionApplicationSource; + use pumpkin_data::data_component_impl::PotionDurationScaleImpl; + use pumpkin_data::item::Item; + use pumpkin_data::item_stack::ItemStack; + + #[test] + fn tipped_arrow_scale_shortens_duration_without_reducing_instant_potency() { + let tipped_arrow = ItemStack::new(1, &Item::TIPPED_ARROW); + let scale = tipped_arrow + .get_data_component::() + .expect("tipped arrows should define a potion duration scale") + .scale; + + assert_eq!(PotionApplicationSource::Arrow.duration_scale(scale), 0.125); + assert_eq!( + (160.0 * PotionApplicationSource::Arrow.duration_scale(scale)) as i32, + 20 + ); + assert_eq!(PotionApplicationSource::Arrow.instant_scale(scale), 1.0); + } +}