Skip to main content

steel_core/behavior/items/
mace.rs

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