feat: add shield & mace

This commit is contained in:
Alexander Medvedev 2026-05-10 16:05:47 +02:00
parent 60bfdf62db
commit 38f6b0fa8b
4 changed files with 118 additions and 11 deletions

View File

@ -11,13 +11,14 @@ use crate::{
world::World,
};
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AttackType {
Knockback,
Critical,
Sweeping,
Strong,
Weak,
MaceSmash,
}
impl AttackType {
@ -27,22 +28,31 @@ impl AttackType {
let sprinting = entity.sprinting.load(Ordering::Relaxed);
let on_ground = entity.on_ground.load(Ordering::Relaxed);
let fall_distance = player.living_entity.fall_distance.load();
let sword = player.inventory().held_item().lock().await.is_sword();
let held_item = player.inventory().held_item();
let is_mace = {
let stack = held_item.lock().await;
stack.item.id == pumpkin_data::item::Item::MACE.id
};
if is_mace && !on_ground && fall_distance > 1.5 {
return Self::MaceSmash;
}
let sword = {
let stack = held_item.lock().await;
stack.is_sword()
};
let is_strong = attack_cooldown_progress > 0.9;
if sprinting && is_strong {
return Self::Knockback;
}
// TODO: even more checks
if is_strong && !on_ground && fall_distance > 0.0 {
// !sprinting omitted
return Self::Critical;
}
// TODO: movement speed check
if sword && is_strong {
// !is_crit, !is_knockback_hit, on_ground omitted
return Self::Sweeping;
}
@ -112,5 +122,10 @@ pub async fn player_attack_sound(pos: &Vector3<f64>, world: &World, attack_type:
.play_sound(Sound::EntityPlayerAttackWeak, SoundCategory::Players, pos)
.await;
}
AttackType::MaceSmash => {
world
.play_sound(Sound::ItemMaceSmashAir, SoundCategory::Players, pos)
.await;
}
}
}

View File

@ -35,11 +35,11 @@ use pumpkin_data::attributes::Attributes;
use pumpkin_data::damage::DeathMessageType;
use pumpkin_data::data_component_impl::Operation;
use pumpkin_data::data_component_impl::{
DeathProtectionImpl, EquipmentSlot, EquippableImpl, FoodImpl,
BlocksAttacksImpl, DeathProtectionImpl, EquipmentSlot, EquippableImpl, FoodImpl,
};
use pumpkin_data::effect::StatusEffect;
use pumpkin_data::entity::{EntityPose, EntityStatus, EntityType};
use pumpkin_data::item_stack::ItemStack;
use pumpkin_data::item_stack::{DamageResult, ItemStack};
use pumpkin_data::sound::SoundCategory;
use pumpkin_data::{Block, translation};
use pumpkin_data::{damage::DamageType, sound::Sound};
@ -252,6 +252,17 @@ impl LivingEntity {
self.set_living_flag(Self::USING_ITEM_FLAG, false).await;
}
pub async fn is_blocking(&self) -> bool {
let item_in_use = self.item_in_use.lock().await;
if let Some(item) = item_in_use.as_ref()
&& item.get_data_component::<BlocksAttacksImpl>().is_some()
{
let use_time = self.item_use_time.load(Ordering::Relaxed);
return item.get_max_use_time() - use_time >= 5;
}
false
}
pub async fn heal(&self, additional_health: f32) {
assert!(additional_health > 0.0);
self.set_health(self.health.load() + additional_health)
@ -1929,6 +1940,56 @@ impl EntityBase for LivingEntity {
// Total damage after reductions
let effective_amount = amount * (1.0 - resistance_reduction);
// Check for shield blocking
if self.is_blocking().await
&& !damage_type.has_tag(&tag::DamageType::MINECRAFT_BYPASSES_SHIELD)
&& let Some(pos) = position
{
let player_pos = self.entity.pos.load();
let look_vec = Vector3::rotation_vector(0.0, self.entity.yaw.load() as f64);
let mut source_to_player = (player_pos - pos).normalize();
source_to_player.y = 0.0;
if source_to_player.dot(&look_vec) < 0.0 {
world
.play_sound(Sound::ItemShieldBlock, SoundCategory::Players, &player_pos)
.await;
let active_hand = self.active_hand.lock().await;
if let Some(hand) = *active_hand {
let slot = if hand == Hand::Left {
EquipmentSlot::MAIN_HAND
} else {
EquipmentSlot::OFF_HAND
};
let equipment_lock = self.entity_equipment.lock().await;
let stack_arc = equipment_lock.get(&slot);
let mut stack = stack_arc.lock().await;
let durability_damage = (amount / 1.0).floor().max(1.0) as i32;
if stack.damage_item(durability_damage) == DamageResult::Broken {
world
.send_entity_status(
&self.entity,
crate::entity::equipment_break_status(&slot),
)
.await;
*stack = ItemStack::EMPTY.clone();
let broken_stack = stack.clone();
drop(stack);
drop(stack_arc);
drop(equipment_lock);
self.send_equipment_changes(&[(slot, broken_stack)]).await;
self.clear_active_hand().await;
}
}
return false;
}
}
// Apply hurt cooldown logic
let last_damage = self.last_damage_taken.load();
let (damage_amount, play_sound) =

View File

@ -818,6 +818,7 @@ impl Player {
//self.world().level.list_cached();
}
#[expect(clippy::too_many_lines)]
pub async fn attack(&self, victim: Arc<dyn EntityBase>) {
let world = self.world();
let server = world.server.upgrade().unwrap();
@ -879,11 +880,21 @@ impl Player {
damage *= 1.5;
}
let is_mace_smash = matches!(attack_type, AttackType::MaceSmash);
if is_mace_smash {
let fall_distance = self.living_entity.fall_distance.load();
damage += 1.5 * f64::from(fall_distance);
}
if !victim
.damage_with_context(
&*victim,
damage as f32,
DamageType::PLAYER_ATTACK,
if is_mace_smash {
DamageType::MACE_SMASH
} else {
DamageType::PLAYER_ATTACK
},
None,
Some(self),
Some(self),
@ -900,6 +911,22 @@ impl Player {
return;
}
if is_mace_smash {
let fall_distance = self.living_entity.fall_distance.load();
self.living_entity.fall_distance.store(0.0);
world
.play_sound(
if fall_distance > 5.0 {
Sound::ItemMaceSmashGroundHeavy
} else {
Sound::ItemMaceSmashGround
},
SoundCategory::Players,
&pos,
)
.await;
}
player_attack_sound(&pos, &world, attack_type).await;
self.living_entity.last_attacking_id.store(

View File

@ -41,7 +41,9 @@ use crate::world::{World, chunker};
use pumpkin_data::block_properties::{
BlockProperties, CommandBlockLikeProperties, WaterLikeProperties,
};
use pumpkin_data::data_component_impl::{ConsumableImpl, EquipmentSlot, EquippableImpl, FoodImpl};
use pumpkin_data::data_component_impl::{
BlocksAttacksImpl, ConsumableImpl, EquipmentSlot, EquippableImpl, FoodImpl,
};
use pumpkin_data::entity::EntityType;
use pumpkin_data::item::Item;
use pumpkin_data::item_stack::ItemStack;
@ -2335,7 +2337,9 @@ impl JavaClient {
}
}
if held.get_data_component::<ConsumableImpl>().is_some() {
if held.get_data_component::<ConsumableImpl>().is_some()
|| held.get_data_component::<BlocksAttacksImpl>().is_some()
{
// If its food we want to make sure we can actually consume it
if let Some(food) = held.get_data_component::<FoodImpl>() {
if player.abilities.lock().await.invulnerable