feat: add Crossbow

This commit is contained in:
Alexander Medvedev 2026-05-10 16:43:42 +02:00
parent 38f6b0fa8b
commit 1abfb195de
7 changed files with 395 additions and 14 deletions

View File

@ -813,7 +813,6 @@ dependencies = [
"serde",
"thiserror",
"tracing",
"uuid",
]
[[package]]

View File

@ -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<Box<dyn DataCompone
StoredEnchantments => 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<NbtCompound>,
}
impl ChargedProjectilesImpl {
fn read_data(data: &NbtTag) -> Option<Self> {
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

View File

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

View File

@ -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::<f32>() < 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

View File

@ -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::<EnchantmentsImpl>()
{
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::<EnchantmentsImpl>()
{
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;
}
}
}
_ => {}
}

View File

@ -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<Box<dyn Future<Output = ()> + 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::<ChargedProjectilesImpl>()
.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<Box<dyn Future<Output = ()> + 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::<EnchantmentsImpl>() {
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<Mutex<ItemStack>>) {
let mut stack = held.lock().await;
let projectiles = stack
.get_data_component::<ChargedProjectilesImpl>()
.cloned();
if let Some(charged) = projectiles {
let mut has_multishot = false;
if let Some(enchantments) = stack.get_data_component::<EnchantmentsImpl>() {
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<dyn EntityBase> = Arc::new(arrow);
world.spawn_entity(arrow_arc).await;
}
}
stack
.patch
.retain(|(id, _)| *id != DataComponent::ChargedProjectiles);
player.damage_held_item(1).await;
}
}
}

View File

@ -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<ItemRegistry> {
manager.register(ArrowItem);
manager.register(BowItem);
manager.register(CrossbowItem);
manager.register(SnowBallItem);
manager.register(HoeItem);
manager.register(EggItem);