160 lines
4.8 KiB
Rust
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);
|
|
}
|
|
}
|