Pumpkin/crates/pumpkin/src/entity/combat.rs
2026-08-09 17:29:54 +02:00

160 lines
4.8 KiB
Rust

use std::sync::atomic::Ordering;
use crate::entity::EntityBase;
use pumpkin_data::{
attributes::Attributes,
particle::Particle,
sound::{Sound, SoundCategory},
};
use pumpkin_util::math::vector3::Vector3;
use crate::{
entity::{Entity, player::Player},
world::World,
};
use crate::net::ClientPlatform;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AttackType {
Knockback,
Critical,
Sweeping,
Strong,
Weak,
MaceSmash,
}
impl AttackType {
pub async fn new(player: &Player, attack_cooldown_progress: f32) -> Self {
let entity = &player.get_entity();
let sprinting = entity.is_sprinting();
let on_ground = entity.on_ground.load(Ordering::Relaxed);
let fall_distance = player.living_entity.fall_distance.load();
let held_item = player.inventory().held_item().await;
let is_mace = held_item.item.id == pumpkin_data::item::Item::MACE.id;
if is_mace && !on_ground && fall_distance > 1.5 {
return Self::MaceSmash;
}
let sword = held_item.is_sword();
let is_bedrock = matches!(player.client.as_ref(), ClientPlatform::Bedrock(_));
let is_strong = attack_cooldown_progress > 0.9;
if sprinting && is_strong {
return Self::Knockback;
}
if is_strong && !on_ground && fall_distance > 0.0 {
return Self::Critical;
}
if sword && is_strong && !is_bedrock {
return Self::Sweeping;
}
if is_strong { Self::Strong } else { Self::Weak }
}
}
/// Scales a knockback `strength` by a living entity's knockback resistance,
/// mirroring vanilla `LivingEntity.knockback`: `strength *= 1.0 - resistance`.
/// A resistance of 1.0 (iron golem, warden, ...) cancels the knockback entirely.
pub fn knockback_after_resistance(strength: f64, resistance: f64) -> f64 {
strength * (1.0 - resistance)
}
pub fn handle_knockback(attacker: &Entity, victim: &dyn EntityBase, strength: f64) {
let resistance = victim.get_living_entity().map_or(0.0, |living| {
living.get_attribute_value(&Attributes::KNOCKBACK_RESISTANCE)
});
let strength = knockback_after_resistance(strength * 0.5, resistance);
if strength > 0.0 {
let yaw = attacker.yaw.load();
victim.get_entity().knockback(
strength,
f64::from((yaw.to_radians()).sin()),
f64::from(-(yaw.to_radians()).cos()),
);
}
let velocity = attacker.velocity.load();
attacker.velocity.store(velocity.multiply(0.6, 1.0, 0.6));
}
pub fn spawn_sweep_particle(attacker_entity: &Entity, world: &World, pos: &Vector3<f64>) {
let yaw = attacker_entity.yaw.load();
let d = -f64::from((yaw.to_radians()).sin());
let e = f64::from((yaw.to_radians()).cos());
let scale = 0.5;
let body_y = f64::from(attacker_entity.height()).mul_add(scale, pos.y);
world.spawn_particle(
Vector3::new(pos.x + d, body_y, pos.z + e),
Vector3::new(0.0, 0.0, 0.0),
0.0,
0,
Particle::SweepAttack,
);
}
pub async fn player_attack_sound(pos: &Vector3<f64>, world: &World, attack_type: AttackType) {
match attack_type {
AttackType::Knockback => {
world.play_sound(
Sound::EntityPlayerAttackKnockback,
SoundCategory::Players,
pos,
);
}
AttackType::Critical => {
world.play_sound(Sound::EntityPlayerAttackCrit, SoundCategory::Players, pos);
}
AttackType::Sweeping => {
world.play_sound(Sound::EntityPlayerAttackSweep, SoundCategory::Players, pos);
}
AttackType::Strong => {
world.play_sound(Sound::EntityPlayerAttackStrong, SoundCategory::Players, pos);
}
AttackType::Weak => {
world.play_sound(Sound::EntityPlayerAttackWeak, SoundCategory::Players, pos);
}
AttackType::MaceSmash => {
world.play_sound(Sound::ItemMaceSmashAir, SoundCategory::Players, pos);
}
}
}
#[cfg(test)]
mod tests {
use super::knockback_after_resistance;
#[test]
fn zero_resistance_keeps_full_strength() {
assert_eq!(knockback_after_resistance(0.4, 0.0), 0.4);
}
#[test]
fn full_resistance_cancels_knockback() {
// Iron golem / warden have KNOCKBACK_RESISTANCE == 1.0.
assert_eq!(knockback_after_resistance(0.4, 1.0), 0.0);
}
#[test]
fn partial_resistance_scales_strength() {
// Ravager has KNOCKBACK_RESISTANCE == 0.75.
assert!((knockback_after_resistance(0.4, 0.75) - 0.1).abs() < 1e-9);
}
#[test]
fn over_full_resistance_is_negative_so_callers_skip_it() {
// Stacked armour modifiers can push resistance above 1.0; the result is
// negative and callers guard on `strength > 0.0`.
assert!(knockback_after_resistance(0.4, 1.2) < 0.0);
}
}