mirror of
https://github.com/Pumpkin-MC/Pumpkin.git
synced 2026-08-30 20:14:23 +00:00
fix(projectile): preserve arrow payloads (#2710)
This commit is contained in:
@@ -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), });
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user