fix(projectile): preserve arrow payloads (#2710)

This commit is contained in:
Eshan I.
2026-08-02 14:08:10 -04:00
committed by GitHub
parent 7844590958
commit 0b2ed1af38
11 changed files with 422 additions and 79 deletions

View File

@@ -133,7 +133,7 @@ pub struct ItemComponents {
#[serde(rename = "minecraft:potion_contents")]
pub potion_contents: Option<serde_json::Value>,
#[serde(rename = "minecraft:potion_duration_scale")]
pub potion_duration_scale: Option<serde_json::Value>,
pub potion_duration_scale: Option<f32>,
#[serde(rename = "minecraft:provides_banner_patterns")]
pub provides_banner_patterns: Option<serde_json::Value>,
#[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), });

View File

@@ -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<Self> {
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<H: std::hash::Hasher>(&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::<PotionDurationScaleImpl>())
.flatten()
})
.expect("tipped arrows should have a duration scale");
assert_eq!(scale.scale, 0.125);
}
}
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct SuspiciousStewEffectsImpl;

View File

@@ -515,6 +515,9 @@ pub fn read_data(id: DataComponent, data: &NbtTag) -> Option<Box<dyn DataCompone
DataComponent::Unbreakable => 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()),

View File

@@ -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),

View File

@@ -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,

View File

@@ -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);
}
}

View File

@@ -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<i32>,
pub item_stack: RwLock<ItemStack>,
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<i32>) -> 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<i32>,
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<ItemStack> {
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::<PotionDurationScaleImpl>()
.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<f64>, 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));
}
}

View File

@@ -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,
])
}
}

View File

@@ -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();

View File

@@ -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<Mutex<ItemStack>>) {
let mut stack = held.lock().await;
let projectiles = stack
.get_data_component::<ChargedProjectilesImpl>()
.cloned();
if let Some(charged) = projectiles {
let (projectiles, has_multishot) = {
let stack = held.lock().await;
let projectiles = stack
.get_data_component::<ChargedProjectilesImpl>()
.cloned();
let has_multishot =
stack
.get_data_component::<EnchantmentsImpl>()
@@ -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<dyn EntityBase> = 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;

View File

@@ -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::<PotionDurationScaleImpl>()
.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);
}
}