From 1abfb195de21e5528a4b0f73a1b72c1f299798ff Mon Sep 17 00:00:00 2001 From: Alexander Medvedev Date: Sun, 10 May 2026 16:43:42 +0200 Subject: [PATCH] feat: add Crossbow --- pumpkin-codegen/Cargo.lock | 1 - pumpkin-data/src/data_component_impl.rs | 45 +++++- pumpkin-data/src/item_stack/categories.rs | 4 - pumpkin/src/entity/living.rs | 101 +++++++++++- pumpkin/src/entity/player.rs | 67 +++++++- pumpkin/src/item/items/crossbow.rs | 188 ++++++++++++++++++++++ pumpkin/src/item/items/mod.rs | 3 + 7 files changed, 395 insertions(+), 14 deletions(-) create mode 100644 pumpkin/src/item/items/crossbow.rs diff --git a/pumpkin-codegen/Cargo.lock b/pumpkin-codegen/Cargo.lock index 8f2b10ea5..2e7015d26 100644 --- a/pumpkin-codegen/Cargo.lock +++ b/pumpkin-codegen/Cargo.lock @@ -813,7 +813,6 @@ dependencies = [ "serde", "thiserror", "tracing", - "uuid", ] [[package]] diff --git a/pumpkin-data/src/data_component_impl.rs b/pumpkin-data/src/data_component_impl.rs index 81705233b..b571b58cf 100644 --- a/pumpkin-data/src/data_component_impl.rs +++ b/pumpkin-data/src/data_component_impl.rs @@ -3,10 +3,10 @@ use crate::attributes::Attributes; use crate::data_component::DataComponent; use crate::data_component::DataComponent::{ - AttributeModifiers, BlocksAttacks, Consumable, CustomData, CustomName, Damage, DamageResistant, - DeathProtection, Enchantable, Enchantments, Equippable, FireworkExplosion, Fireworks, Food, - ItemModel, ItemName, JukeboxPlayable, MapId, MaxDamage, MaxStackSize, PotionContents, - StoredEnchantments, Tool, Unbreakable, UseCooldown, Weapon, + AttributeModifiers, BlocksAttacks, ChargedProjectiles, Consumable, CustomData, CustomName, + Damage, DamageResistant, DeathProtection, Enchantable, Enchantments, Equippable, + FireworkExplosion, Fireworks, Food, ItemModel, ItemName, JukeboxPlayable, MapId, MaxDamage, + MaxStackSize, PotionContents, StoredEnchantments, Tool, Unbreakable, UseCooldown, Weapon, }; use crate::effect::{self, StatusEffect}; use crate::entity_type::EntityType; @@ -67,6 +67,9 @@ pub fn read_data(id: DataComponent, data: &NbtTag) -> Option Some(StoredEnchantmentsImpl::read_data(data)?.to_dyn()), UseCooldown => Some(UseCooldownImpl::read_data(data)?.to_dyn()), MapId => Some(MapIdImpl::read_data(data)?.to_dyn()), + DataComponent::ChargedProjectiles => { + Some(ChargedProjectilesImpl::read_data(data)?.to_dyn()) + } _ => None, } } @@ -1522,8 +1525,38 @@ impl DataComponentImpl for MapIdImpl { pub struct MapDecorationsImpl; #[derive(Clone, Debug, Hash, PartialEq, Eq)] pub struct MapPostProcessingImpl; -#[derive(Clone, Debug, Hash, PartialEq, Eq)] -pub struct ChargedProjectilesImpl; +#[derive(Clone, Debug, PartialEq)] +pub struct ChargedProjectilesImpl { + pub projectiles: Vec, +} + +impl ChargedProjectilesImpl { + fn read_data(data: &NbtTag) -> Option { + let list = data.extract_list()?; + let mut projectiles = Vec::new(); + for item in list { + projectiles.push(item.extract_compound()?.clone()); + } + Some(Self { projectiles }) + } +} + +impl DataComponentImpl for ChargedProjectilesImpl { + fn write_data(&self) -> NbtTag { + let mut list = Vec::new(); + for item in &self.projectiles { + list.push(NbtTag::Compound(item.clone())); + } + NbtTag::List(list) + } + + fn get_hash(&self) -> i32 { + 0 + } + + default_impl!(ChargedProjectiles); +} + #[derive(Clone, Debug, Hash, PartialEq, Eq)] pub struct BundleContentsImpl; /// Status effect instance for potion contents diff --git a/pumpkin-data/src/item_stack/categories.rs b/pumpkin-data/src/item_stack/categories.rs index f49792133..d0597a50a 100644 --- a/pumpkin-data/src/item_stack/categories.rs +++ b/pumpkin-data/src/item_stack/categories.rs @@ -50,28 +50,24 @@ impl ItemStack { /// Test-only predicates: identify 2-durability tools (axes/pickaxes/shovels/hoes). /// In production, durability cost is data-driven via the `Weapon` component. /// These helpers exist only to validate item categorization in tests. - #[cfg(test)] #[inline] #[must_use] pub fn is_axe(&self) -> bool { self.item.has_tag(&tag::Item::MINECRAFT_AXES) } - #[cfg(test)] #[inline] #[must_use] pub fn is_pickaxe(&self) -> bool { self.item.has_tag(&tag::Item::MINECRAFT_PICKAXES) } - #[cfg(test)] #[inline] #[must_use] pub fn is_shovel(&self) -> bool { self.item.has_tag(&tag::Item::MINECRAFT_SHOVELS) } - #[cfg(test)] #[inline] #[must_use] pub fn is_hoe(&self) -> bool { diff --git a/pumpkin/src/entity/living.rs b/pumpkin/src/entity/living.rs index ce142d374..1c6ec94cf 100644 --- a/pumpkin/src/entity/living.rs +++ b/pumpkin/src/entity/living.rs @@ -20,8 +20,7 @@ use std::{collections::HashMap, sync::atomic::AtomicI32}; use tracing::warn; use super::experience_orb::ExperienceOrbEntity; -use super::{Entity, NBTStorage}; -use super::{EntityBase, NBTStorageInit}; +use super::{Entity, EntityBase, NBTStorage}; use crate::block::OnLandedUponArgs; use crate::entity::attributes::AttributeInstance; use crate::entity::attributes::Modifier; @@ -1955,6 +1954,39 @@ impl EntityBase for LivingEntity { .play_sound(Sound::ItemShieldBlock, SoundCategory::Players, &player_pos) .await; + if let Some(attacker_player) = cause.and_then(|c| c.get_player()) { + let held_item = attacker_player.inventory().held_item(); + let is_axe = held_item.lock().await.is_axe(); + if is_axe { + let mut disable_chance = 0.25; + let is_sprinting = attacker_player + .living_entity + .entity + .sprinting + .load(Ordering::Relaxed); + if is_sprinting { + disable_chance = 1.0; + } + + if rand::random::() < disable_chance { + if let Some(victim_player) = caller.get_player() { + victim_player + .start_cooldown("minecraft:shield".to_string(), 100) + .await; + self.clear_active_hand().await; + + use pumpkin_protocol::java::client::play::CEntityStatus; + world + .broadcast_packet_all(&CEntityStatus::new( + self.entity.entity_id, + 30, + )) + .await; + } + } + } + } + let active_hand = self.active_hand.lock().await; if let Some(hand) = *active_hand { let slot = if hand == Hand::Left { @@ -2215,6 +2247,71 @@ impl EntityBase for LivingEntity { .hunger_manager .eat(player, food.nutrition as u8, food.saturation) .await; + + // Special food effects + if item.item == &Item::GOLDEN_APPLE { + self.add_effect(pumpkin_data::potion::Effect { + effect_type: &pumpkin_data::effect::StatusEffect::REGENERATION, + amplifier: 1, + duration: 100, + ambient: false, + show_particles: true, + show_icon: true, + blend: false, + }) + .await; + self.add_effect(pumpkin_data::potion::Effect { + effect_type: &pumpkin_data::effect::StatusEffect::ABSORPTION, + amplifier: 0, + duration: 2400, + ambient: false, + show_particles: true, + show_icon: true, + blend: false, + }) + .await; + } else if item.item == &Item::ENCHANTED_GOLDEN_APPLE { + self.add_effect(pumpkin_data::potion::Effect { + effect_type: &pumpkin_data::effect::StatusEffect::REGENERATION, + amplifier: 1, + duration: 400, + ambient: false, + show_particles: true, + show_icon: true, + blend: false, + }) + .await; + self.add_effect(pumpkin_data::potion::Effect { + effect_type: &pumpkin_data::effect::StatusEffect::ABSORPTION, + amplifier: 3, + duration: 2400, + ambient: false, + show_particles: true, + show_icon: true, + blend: false, + }) + .await; + self.add_effect(pumpkin_data::potion::Effect { + effect_type: &pumpkin_data::effect::StatusEffect::RESISTANCE, + amplifier: 0, + duration: 6000, + ambient: false, + show_particles: true, + show_icon: true, + blend: false, + }) + .await; + self.add_effect(pumpkin_data::potion::Effect { + effect_type: &pumpkin_data::effect::StatusEffect::FIRE_RESISTANCE, + amplifier: 0, + duration: 6000, + ambient: false, + show_particles: true, + show_icon: true, + blend: false, + }) + .await; + } } // Handle potion consumption diff --git a/pumpkin/src/entity/player.rs b/pumpkin/src/entity/player.rs index d867245f2..2a88c71fe 100644 --- a/pumpkin/src/entity/player.rs +++ b/pumpkin/src/entity/player.rs @@ -35,7 +35,7 @@ use uuid::Uuid; use pumpkin_data::attributes::Attributes; use pumpkin_data::block_properties::{BlockProperties, HorizontalFacing}; use pumpkin_data::damage::DamageType; -use pumpkin_data::data_component_impl::{AttributeModifiersImpl, Operation}; +use pumpkin_data::data_component_impl::{AttributeModifiersImpl, EnchantmentsImpl, Operation}; use pumpkin_data::data_component_impl::{EquipmentSlot, EquippableImpl, ToolImpl, WeaponImpl}; use pumpkin_data::effect::StatusEffect; use pumpkin_data::entity::{EntityPose, EntityStatus, EntityType}; @@ -873,6 +873,23 @@ impl Player { // Modify the added damage based on the multiplier. let mut damage = base_damage + add_damage * damage_multiplier; + + if let Some(strength) = self + .living_entity + .get_effect(&pumpkin_data::effect::StatusEffect::STRENGTH) + .await + { + damage += 3.0 * (f64::from(strength.amplifier) + 1.0); + } + if let Some(weakness) = self + .living_entity + .get_effect(&pumpkin_data::effect::StatusEffect::WEAKNESS) + .await + { + damage -= 4.0 * (f64::from(weakness.amplifier) + 1.0); + } + damage = damage.max(0.0); + let pos = victim_entity.pos.load(); let attack_type = AttackType::new(self, attack_cooldown_progress as f32).await; @@ -911,6 +928,18 @@ impl Player { return; } + if let Some(enchantments) = item_stack + .lock() + .await + .get_data_component::() + { + for (enchantment, level) in enchantments.enchantment.iter() { + if **enchantment == Enchantment::FIRE_ASPECT { + victim_entity.set_on_fire_for_ticks(*level as u32 * 80); + } + } + } + if is_mace_smash { let fall_distance = self.living_entity.fall_distance.load(); self.living_entity.fall_distance.store(0.0); @@ -947,6 +976,42 @@ impl Player { AttackType::Knockback => knockback_strength += 1.0, AttackType::Sweeping => { combat::spawn_sweep_particle(attacker_entity, &world, &pos).await; + + let mut sweep_damage = 1.0; + if let Some(enchantments) = item_stack + .lock() + .await + .get_data_component::() + { + for (enchantment, level) in enchantments.enchantment.iter() { + if **enchantment == Enchantment::SWEEPING_EDGE { + sweep_damage += + damage as f32 * (*level as f32 / (*level as f32 + 1.0)); + } + } + } + + let search_box = BoundingBox::new( + Vector3::new(pos.x - 1.0, pos.y - 0.5, pos.z - 1.0), + Vector3::new(pos.x + 1.0, pos.y + 0.5, pos.z + 1.0), + ); + let victims = world.get_all_at_box(&search_box); + for other_victim in victims { + if other_victim.get_entity().entity_id != victim_entity.entity_id + && other_victim.get_entity().entity_id != attacker_entity.entity_id + { + other_victim + .damage_with_context( + other_victim.as_ref(), + sweep_damage, + DamageType::PLAYER_ATTACK, + None, + Some(self), + Some(self), + ) + .await; + } + } } _ => {} } diff --git a/pumpkin/src/item/items/crossbow.rs b/pumpkin/src/item/items/crossbow.rs new file mode 100644 index 000000000..080926dca --- /dev/null +++ b/pumpkin/src/item/items/crossbow.rs @@ -0,0 +1,188 @@ +use std::any::Any; +use std::future::Future; +use std::pin::Pin; +use std::sync::Arc; +use std::sync::atomic::Ordering; +use tokio::sync::Mutex; + +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::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}; +use pumpkin_util::GameMode; +use pumpkin_world::inventory::Inventory; + +pub struct CrossbowItem; + +impl ItemMetadata for CrossbowItem { + fn ids() -> Box<[u16]> { + Box::new([Item::CROSSBOW.id]) + } +} + +impl ItemBehaviour for CrossbowItem { + 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(); + + if stack + .get_data_component::() + .is_some() + { + Self::fire_projectiles(player, &held).await; + return; + } + + let has_arrows = player.find_arrow().await.is_some(); + if !has_arrows && player.gamemode.load() != GameMode::Creative { + return; + } + + 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(Ordering::Relaxed); + let use_ticks = 72000 - use_ticks; + + let mut charge_time = 25; + 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::QUICK_CHARGE { + charge_time -= 5 * level; + } + } + } + drop(stack); + charge_time = charge_time.max(0); + + if use_ticks >= charge_time { + let arrow_slot = player.find_arrow().await; + if let Some(slot) = arrow_slot { + let inventory = player.inventory(); + let mut stack = held.lock().await; + + 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); + drop(arrow_stack); + + stack.patch.push(( + DataComponent::ChargedProjectiles, + Some(Box::new(ChargedProjectilesImpl { + projectiles: vec![arrow_nbt], + })), + )); + + if player.gamemode.load() != GameMode::Creative { + player.consume_arrow(slot).await; + } + + player + .world() + .play_sound( + Sound::ItemCrossbowLoadingEnd, + SoundCategory::Players, + &player.position(), + ) + .await; + } + } + player.living_entity.clear_active_hand().await; + }) + } + + fn get_use_duration(&self) -> i32 { + 72000 + } + + fn as_any(&self) -> &dyn Any { + self + } +} + +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 mut has_multishot = false; + if let Some(enchantments) = stack.get_data_component::() { + has_multishot = enchantments + .enchantment + .iter() + .any(|(e, _)| **e == pumpkin_data::Enchantment::MULTISHOT); + } + + let world = player.world(); + world + .play_sound( + Sound::ItemCrossbowShoot, + SoundCategory::Players, + &player.position(), + ) + .await; + + let yaw = player.living_entity.entity.yaw.load(); + let pitch = player.living_entity.entity.pitch.load(); + + for _ in charged.projectiles { + let yaws = if has_multishot { + vec![yaw - 10.0, yaw, yaw + 10.0] + } else { + vec![yaw] + }; + + for t_yaw in yaws { + let arrow_entity = + Entity::new(world.clone(), player.position(), &EntityType::ARROW); + let pickup = if player.gamemode.load() == GameMode::Creative { + ArrowPickup::CreativeOnly + } else { + ArrowPickup::Allowed + }; + + let arrow = + ArrowEntity::new_shot(arrow_entity, &player.living_entity.entity, pickup) + .await; + 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 + .patch + .retain(|(id, _)| *id != DataComponent::ChargedProjectiles); + player.damage_held_item(1).await; + } + } +} diff --git a/pumpkin/src/item/items/mod.rs b/pumpkin/src/item/items/mod.rs index e2d2aa2a1..e722e733c 100644 --- a/pumpkin/src/item/items/mod.rs +++ b/pumpkin/src/item/items/mod.rs @@ -4,6 +4,7 @@ pub mod axe; pub mod boat; pub mod bow; pub mod bucket; +pub mod crossbow; pub mod dye; pub mod egg; pub mod end_crystal; @@ -43,6 +44,7 @@ use arrow::ArrowItem; use axe::AxeItem; use bow::BowItem; use bucket::{EmptyBucketItem, FilledBucketItem}; +use crossbow::CrossbowItem; use dye::DyeItem; use egg::EggItem; use ender_eye::EnderEyeItem; @@ -66,6 +68,7 @@ pub fn default_registry() -> Arc { manager.register(ArrowItem); manager.register(BowItem); + manager.register(CrossbowItem); manager.register(SnowBallItem); manager.register(HoeItem); manager.register(EggItem);