Skip to main content

steel_core/behavior/blocks/building/
honey_block.rs

1use 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/// Behavior for honey blocks.
32#[block_behavior]
33pub struct HoneyBlock {
34    block: BlockRef,
35}
36
37impl HoneyBlock {
38    /// Creates a honey block behavior.
39    #[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            // TODO: Award the honey-block slide advancement once advancements exist.
202            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}