feat: add Crossbow
This commit is contained in:
parent
38f6b0fa8b
commit
1abfb195de
1
pumpkin-codegen/Cargo.lock
generated
1
pumpkin-codegen/Cargo.lock
generated
@ -813,7 +813,6 @@ dependencies = [
|
||||
"serde",
|
||||
"thiserror",
|
||||
"tracing",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
188
pumpkin/src/item/items/crossbow.rs
Normal file
188
pumpkin/src/item/items/crossbow.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user