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