steel_core/behavior/blocks/building/
honey_block.rs1use std::sync::Arc;
2
3use glam::DVec3;
4use steel_macros::block_behavior;
5use steel_registry::blocks::{BlockRef, block_state_ext::BlockStateExt as _, shapes::VoxelShape};
6use steel_registry::{sound_events, vanilla_damage_types, vanilla_entities};
7use steel_utils::entity_events::EntityStatus;
8use steel_utils::{BlockLocalAabb, BlockPos, BlockStateId};
9
10use crate::{
11 behavior::{
12 BlockBehavior, BlockCollisionContext, BlockPlaceContext, EntityFallDamage,
13 EntityFallOnContext,
14 },
15 entity::{Entity, InsideBlockEffectCollector, damage::DamageSource},
16 world::{LevelReader, World},
17};
18
19const SLIDE_STARTS_WHEN_VERTICAL_SPEED_IS_AT_LEAST: f64 = 0.13;
20const MIN_FALL_SPEED_TO_BE_CONSIDERED_SLIDING: f64 = 0.08;
21const THROTTLE_SLIDE_SPEED_TO: f64 = 0.05;
22const HONEY_BLOCK_TOP: f64 = 0.9375;
23const HONEY_EDGE_OVERLAP_BASE: f64 = 0.4375;
24const SLIDE_EPSILON: f64 = 1.0e-7;
25const VANILLA_ENTITY_DRAG: f64 = 0.980_000_019_073_486_3;
26const SHAPE_BOXES: &[BlockLocalAabb] = &[BlockLocalAabb::new(
27 0.0625, 0.0, 0.0625, 0.9375, 0.9375, 0.9375,
28)];
29const SHAPE: VoxelShape = VoxelShape::from_boxes(SHAPE_BOXES);
30
31#[block_behavior]
33pub struct HoneyBlock {
34 block: BlockRef,
35}
36
37impl HoneyBlock {
38 #[must_use]
40 pub const fn new(block: BlockRef) -> Self {
41 Self { block }
42 }
43
44 #[must_use]
45 fn old_delta_y(delta_y: f64) -> f64 {
46 delta_y / VANILLA_ENTITY_DRAG + 0.08
47 }
48
49 #[must_use]
50 fn new_delta_y(delta_y: f64) -> f64 {
51 (delta_y - 0.08) * VANILLA_ENTITY_DRAG
52 }
53
54 #[must_use]
55 fn is_sliding_down_at(
56 pos: BlockPos,
57 entity_position: DVec3,
58 velocity: DVec3,
59 bounding_box_width: f64,
60 on_ground: bool,
61 ) -> bool {
62 if on_ground {
63 return false;
64 }
65 if entity_position.y > f64::from(pos.y()) + HONEY_BLOCK_TOP - SLIDE_EPSILON {
66 return false;
67 }
68 if Self::old_delta_y(velocity.y) >= -MIN_FALL_SPEED_TO_BE_CONSIDERED_SLIDING {
69 return false;
70 }
71
72 let dx = (f64::from(pos.x()) + 0.5 - entity_position.x).abs();
73 let dz = (f64::from(pos.z()) + 0.5 - entity_position.z).abs();
74 let overlap_distance = HONEY_EDGE_OVERLAP_BASE + bounding_box_width / 2.0;
75 dx + SLIDE_EPSILON > overlap_distance || dz + SLIDE_EPSILON > overlap_distance
76 }
77
78 #[must_use]
79 fn is_sliding_down(pos: BlockPos, entity: &dyn Entity) -> bool {
80 Self::is_sliding_down_at(
81 pos,
82 entity.position(),
83 entity.velocity(),
84 entity.bounding_box().width(),
85 entity.on_ground(),
86 )
87 }
88
89 #[must_use]
90 fn velocity_after_slide(velocity: DVec3) -> DVec3 {
91 let old_delta_y = Self::old_delta_y(velocity.y);
92 let y = Self::new_delta_y(-THROTTLE_SLIDE_SPEED_TO);
93 if old_delta_y < -SLIDE_STARTS_WHEN_VERTICAL_SPEED_IS_AT_LEAST {
94 let horizontal_reduction_factor = -THROTTLE_SLIDE_SPEED_TO / old_delta_y;
95 return DVec3::new(
96 velocity.x * horizontal_reduction_factor,
97 y,
98 velocity.z * horizontal_reduction_factor,
99 );
100 }
101
102 DVec3::new(velocity.x, y, velocity.z)
103 }
104
105 #[must_use]
106 fn does_entity_do_slide_effects(entity: &dyn Entity) -> bool {
107 if entity.is_living_entity()
108 || entity.entity_type().is_abstract_boat
109 || entity.entity_type().is_abstract_minecart
110 {
111 return true;
112 }
113
114 entity.entity_type() == &vanilla_entities::TNT
115 }
116
117 fn do_slide_movement(entity: &dyn Entity) {
118 entity.set_velocity(Self::velocity_after_slide(entity.velocity()));
119 entity.reset_fall_distance();
120 }
121
122 fn maybe_do_slide_effects(_world: &World, entity: &dyn Entity) {
123 if !Self::does_entity_do_slide_effects(entity) {
124 return;
125 }
126
127 let play_sound = rand::random_range(0..5) == 0;
128 let broadcast_particles = rand::random_range(0..5) == 0;
129 if play_sound {
130 entity.play_sound(&sound_events::BLOCK_HONEY_BLOCK_SLIDE, 1.0, 1.0);
131 }
132 if broadcast_particles {
133 entity.broadcast_entity_event(EntityStatus::HoneySlide);
134 }
135 }
136}
137
138impl BlockBehavior for HoneyBlock {
139 fn get_state_for_placement(&self, _context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
140 Some(self.block.default_state())
141 }
142
143 fn get_collision_shape(
144 &self,
145 _state: BlockStateId,
146 _world: &dyn LevelReader,
147 _pos: BlockPos,
148 _context: BlockCollisionContext,
149 ) -> VoxelShape {
150 SHAPE
151 }
152
153 fn fall_on(
154 &self,
155 _state: BlockStateId,
156 _world: &Arc<World>,
157 _pos: BlockPos,
158 context: EntityFallOnContext<'_>,
159 ) -> Option<EntityFallDamage> {
160 if let Some(entity) = context.source_entity() {
161 entity.play_sound(&sound_events::BLOCK_HONEY_BLOCK_SLIDE, 1.0, 1.0);
162 entity.broadcast_entity_event(EntityStatus::HoneyJump);
163 }
164
165 Some(EntityFallDamage::new(
166 context.fall_distance,
167 0.2,
168 DamageSource::environment(&vanilla_damage_types::FALL),
169 ))
170 }
171
172 fn after_fall_on_damage(
173 &self,
174 state: BlockStateId,
175 _world: &Arc<World>,
176 _pos: BlockPos,
177 entity: &dyn Entity,
178 _fall_damage: &EntityFallDamage,
179 damage_applied: bool,
180 ) {
181 if damage_applied {
182 let sound_type = state.get_block().config.sound_type;
183 entity.play_sound(
184 sound_type.fall_sound,
185 sound_type.volume * 0.5,
186 sound_type.pitch * 0.75,
187 );
188 }
189 }
190
191 fn entity_inside(
192 &self,
193 state: BlockStateId,
194 world: &Arc<World>,
195 pos: BlockPos,
196 entity: &dyn Entity,
197 effect_collector: &mut InsideBlockEffectCollector,
198 is_precise: bool,
199 ) {
200 if Self::is_sliding_down(pos, entity) {
201 Self::do_slide_movement(entity);
203 Self::maybe_do_slide_effects(world, entity);
204 }
205
206 self.default_entity_inside(state, world, pos, entity, effect_collector, is_precise);
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213
214 fn pos() -> BlockPos {
215 BlockPos::new(10, 64, 20)
216 }
217
218 #[test]
219 fn slide_requires_entity_to_be_airborne() {
220 assert!(!HoneyBlock::is_sliding_down_at(
221 pos(),
222 DVec3::new(10.95, 64.5, 20.5),
223 DVec3::new(0.0, -0.3, 0.0),
224 0.6,
225 true,
226 ));
227 }
228
229 #[test]
230 fn slide_requires_entity_below_honey_top() {
231 assert!(!HoneyBlock::is_sliding_down_at(
232 pos(),
233 DVec3::new(10.95, 64.9375, 20.5),
234 DVec3::new(0.0, -0.3, 0.0),
235 0.6,
236 false,
237 ));
238 }
239
240 #[test]
241 fn slide_requires_fast_enough_downward_motion() {
242 assert!(!HoneyBlock::is_sliding_down_at(
243 pos(),
244 DVec3::new(10.95, 64.5, 20.5),
245 DVec3::new(0.0, -0.1, 0.0),
246 0.6,
247 false,
248 ));
249 }
250
251 #[test]
252 fn slide_requires_entity_overlapping_honey_edge() {
253 assert!(!HoneyBlock::is_sliding_down_at(
254 pos(),
255 DVec3::new(10.5, 64.5, 20.5),
256 DVec3::new(0.0, -0.3, 0.0),
257 0.6,
258 false,
259 ));
260 assert!(HoneyBlock::is_sliding_down_at(
261 pos(),
262 DVec3::new(11.25, 64.5, 20.5),
263 DVec3::new(0.0, -0.3, 0.0),
264 0.6,
265 false,
266 ));
267 }
268
269 #[test]
270 fn slide_throttles_fast_fall_horizontal_velocity() {
271 let velocity = HoneyBlock::velocity_after_slide(DVec3::new(0.8, -0.3, -0.4));
272 let old_delta_y = HoneyBlock::old_delta_y(-0.3);
273 let scale = -0.05 / old_delta_y;
274
275 assert_eq!(
276 velocity,
277 DVec3::new(0.8 * scale, HoneyBlock::new_delta_y(-0.05), -0.4 * scale,)
278 );
279 }
280
281 #[test]
282 fn slide_preserves_horizontal_velocity_when_fall_is_not_fast() {
283 let velocity = HoneyBlock::velocity_after_slide(DVec3::new(0.8, -0.18, -0.4));
284
285 assert_eq!(
286 velocity,
287 DVec3::new(0.8, HoneyBlock::new_delta_y(-0.05), -0.4)
288 );
289 }
290}