steel_core/behavior/items/
mace.rs1use 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#[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}