Skip to main content

steel_core/behavior/items/
mace.rs

1use std::sync::Arc;
2
3use glam::DVec3;
4use steel_macros::item_behavior;
5use steel_protocol::packets::game::CSetEntityMotion;
6use steel_registry::item_stack::ItemStack;
7use steel_registry::sound_event::SoundEventRef;
8use steel_registry::{level_events, sound_events, vanilla_damage_types};
9
10use crate::behavior::ItemBehavior;
11use crate::enchantment_helper::{self, EnchantmentDamageContext};
12use crate::entity::damage::DamageSource;
13use crate::entity::{Entity, LivingEntity, SharedEntity};
14use crate::inventory::equipment::EquipmentSlot;
15
16/// Vanilla mace item combat behavior.
17#[item_behavior]
18pub struct MaceItem;
19
20impl MaceItem {
21    const SMASH_ATTACK_FALL_THRESHOLD: f64 = 1.5;
22    const SMASH_ATTACK_HEAVY_THRESHOLD: f64 = 5.0;
23    const SMASH_ATTACK_KNOCKBACK_RADIUS: f64 = 3.5;
24    const SMASH_ATTACK_KNOCKBACK_POWER: f64 = 0.7;
25    const SMASH_ATTACK_PARTICLE_DATA: i32 = 750;
26
27    fn can_smash_attack(attacker: &dyn LivingEntity) -> bool {
28        attacker.fall_distance() > Self::SMASH_ATTACK_FALL_THRESHOLD && !attacker.is_fall_flying()
29    }
30
31    fn calculate_impact_position(attacker: &dyn LivingEntity) -> DVec3 {
32        if let Some(impact_pos) = attacker.current_impulse_impact_pos()
33            && impact_pos.y <= attacker.position().y
34        {
35            return impact_pos;
36        }
37
38        attacker.position()
39    }
40
41    fn smash_sound(target: &dyn LivingEntity, attacker: &dyn LivingEntity) -> SoundEventRef {
42        if !target.on_ground() {
43            return &sound_events::ITEM_MACE_SMASH_AIR;
44        }
45
46        if attacker.fall_distance() > Self::SMASH_ATTACK_HEAVY_THRESHOLD {
47            &sound_events::ITEM_MACE_SMASH_GROUND_HEAVY
48        } else {
49            &sound_events::ITEM_MACE_SMASH_GROUND
50        }
51    }
52
53    fn knockback_power(
54        attacker: &dyn LivingEntity,
55        nearby: &dyn LivingEntity,
56        direction: DVec3,
57    ) -> f64 {
58        let heavy_multiplier = if attacker.fall_distance() > Self::SMASH_ATTACK_HEAVY_THRESHOLD {
59            2.0
60        } else {
61            1.0
62        };
63        (Self::SMASH_ATTACK_KNOCKBACK_RADIUS - direction.length())
64            * Self::SMASH_ATTACK_KNOCKBACK_POWER
65            * heavy_multiplier
66            * (1.0 - nearby.knockback_resistance())
67    }
68
69    fn should_knockback(
70        attacker: &dyn LivingEntity,
71        target: &dyn LivingEntity,
72        nearby: &dyn LivingEntity,
73    ) -> bool {
74        if nearby.is_spectator() || nearby.id() == attacker.id() || nearby.id() == target.id() {
75            return false;
76        }
77        if attacker.is_allied_to(nearby) || nearby.is_tame_owned_by(target) {
78            return false;
79        }
80        if nearby.is_marker_armor_stand() {
81            return false;
82        }
83        if nearby
84            .as_player()
85            .is_some_and(|player| player.has_infinite_materials() && player.is_flying())
86        {
87            return false;
88        }
89
90        target.position().distance_squared(nearby.position())
91            <= Self::SMASH_ATTACK_KNOCKBACK_RADIUS * Self::SMASH_ATTACK_KNOCKBACK_RADIUS
92    }
93
94    fn apply_knockback(attacker: &dyn LivingEntity, target: &dyn LivingEntity) {
95        let Some(world) = attacker.level() else {
96            return;
97        };
98
99        let event_pos = target
100            .on_pos(1.0e-5)
101            .unwrap_or_else(|| target.block_position());
102        world.level_event(
103            level_events::PARTICLES_SMASH_ATTACK,
104            event_pos,
105            Self::SMASH_ATTACK_PARTICLE_DATA,
106            None,
107        );
108
109        let search_box = target
110            .bounding_box()
111            .inflate(Self::SMASH_ATTACK_KNOCKBACK_RADIUS);
112        for nearby in world.get_entities_in_aabb_matching(&search_box, |entity| {
113            let Some(nearby) = entity.as_living_entity() else {
114                return false;
115            };
116            Self::should_knockback(attacker, target, nearby)
117        }) {
118            let Some(nearby_living) = nearby.as_living_entity() else {
119                continue;
120            };
121            let direction = nearby_living.position() - target.position();
122            let knockback_power = Self::knockback_power(attacker, nearby_living, direction);
123            if knockback_power <= 0.0 {
124                continue;
125            }
126
127            let horizontal = if direction.length_squared() > 0.0 {
128                direction.normalize() * knockback_power
129            } else {
130                DVec3::ZERO
131            };
132            nearby_living.push_impulse(DVec3::new(horizontal.x, 0.7, horizontal.z));
133            if let Some(player) = nearby_living.as_player() {
134                player.send_packet(CSetEntityMotion::new(
135                    nearby_living.id(),
136                    nearby_living.velocity(),
137                ));
138            }
139        }
140    }
141}
142
143impl ItemBehavior for MaceItem {
144    fn get_item_damage_source(&self, attacker: &SharedEntity) -> Option<DamageSource> {
145        let living = attacker.as_living_entity()?;
146        Self::can_smash_attack(living)
147            .then(|| DamageSource::direct(&vanilla_damage_types::MACE_SMASH, Arc::clone(attacker)))
148    }
149
150    fn get_attack_damage_bonus(
151        &self,
152        attacker: &dyn LivingEntity,
153        victim: &dyn Entity,
154        _base_damage: f32,
155        damage_source: &DamageSource,
156    ) -> f32 {
157        if !Self::can_smash_attack(attacker) {
158            return 0.0;
159        }
160
161        let fall_distance = attacker.fall_distance();
162        let damage = if fall_distance <= 3.0 {
163            4.0 * fall_distance
164        } else if fall_distance <= 8.0 {
165            12.0 + 2.0 * (fall_distance - 3.0)
166        } else {
167            22.0 + fall_distance - 8.0
168        };
169        let context = EnchantmentDamageContext::new(
170            victim.entity_type(),
171            Some(attacker.entity_type()),
172            Some(attacker.entity_type()),
173            damage_source,
174        );
175        let mut damage_per_fallen_block = 0.0;
176        attacker.with_equipment_slot(EquipmentSlot::MainHand, &mut |item| {
177            damage_per_fallen_block =
178                enchantment_helper::modify_smash_damage_per_fallen_block(item, &context, 0.0);
179        });
180        (damage + f64::from(damage_per_fallen_block) * fall_distance) as f32
181    }
182
183    fn hurt_enemy(
184        &self,
185        _stack: &mut ItemStack,
186        target: &dyn LivingEntity,
187        attacker: &dyn LivingEntity,
188    ) {
189        if !Self::can_smash_attack(attacker) {
190            return;
191        }
192
193        let velocity = attacker.velocity();
194        attacker.set_velocity(DVec3::new(velocity.x, 0.01, velocity.z));
195        attacker.set_ignore_fall_damage_from_current_impulse(
196            true,
197            Self::calculate_impact_position(attacker),
198        );
199        if let Some(player) = attacker.as_player() {
200            player.send_packet(CSetEntityMotion::new(attacker.id(), attacker.velocity()));
201        }
202
203        if let Some(world) = attacker.level() {
204            world.play_sound_at(
205                Self::smash_sound(target, attacker),
206                attacker.sound_source(),
207                attacker.position(),
208                1.0,
209                1.0,
210                None,
211            );
212        }
213        Self::apply_knockback(attacker, target);
214    }
215
216    fn post_hurt_enemy(
217        &self,
218        _stack: &mut ItemStack,
219        _target: &dyn LivingEntity,
220        attacker: &dyn LivingEntity,
221    ) {
222        if Self::can_smash_attack(attacker) {
223            attacker.reset_fall_distance();
224        }
225    }
226}