Skip to main content

steel_core/behavior/blocks/building/
bed_block.rs

1use std::sync::Arc;
2
3use crate::{
4    behavior::{
5        BlockBehavior, BlockHitResult, BlockPlaceContext, BlockStateBehaviorExt as _,
6        EntityFallDamage, EntityFallOnContext, EntityLandingContext, InteractionResult,
7        InventoryAccess, PlacementSource,
8    },
9    entity::{Entity, ai::path::PathComputationType, dismount_helper},
10    player::Player,
11    world::{ScheduledTickAccess, World},
12};
13use glam::DVec3;
14use steel_macros::block_behavior;
15use steel_registry::blocks::properties::{BedPart, BoolProperty, EnumProperty};
16use steel_registry::blocks::{
17    BlockRef, block_state_ext::BlockStateExt, properties::BlockStateProperties,
18};
19use steel_registry::vanilla_blocks;
20use steel_utils::{BlockPos, BlockStateId, Direction, types::UpdateFlags};
21use text_components::TextComponent;
22use text_components::translation::TranslatedMessage;
23
24const BED_BOUNCE_SCALE: f64 = 0.660_000_026_226_043_7;
25const BED_PART: &EnumProperty<BedPart> = &BlockStateProperties::BED_PART;
26const FACING: &EnumProperty<Direction> = &BlockStateProperties::HORIZONTAL_FACING;
27const OCCUPIED: &BoolProperty = &BlockStateProperties::OCCUPIED;
28/// Behavior for beds
29///
30/// TODO: Mirror vanilla `BedBlock.useWithoutItem` invalid-dimension explosion
31/// once Steel has a strict `World::explode` foundation: show the bed-rule error
32/// message, remove both bed halves, and use bad-respawn-point explosion damage.
33/// TODO: Mirror vanilla `BedBlock.kickVillagerOutOfBed` once villager sleeping
34/// entities exist.
35#[block_behavior]
36pub struct BedBlock {
37    block: BlockRef,
38}
39
40impl BedBlock {
41    /// Creates a bed block behavior.
42    #[must_use]
43    pub const fn new(block: BlockRef) -> Self {
44        Self { block }
45    }
46
47    #[must_use]
48    fn fall_context(context: EntityFallOnContext<'_>) -> EntityFallOnContext<'_> {
49        context.with_fall_distance(context.fall_distance * 0.5)
50    }
51
52    #[must_use]
53    fn velocity_after_fall(context: EntityLandingContext) -> DVec3 {
54        if context.velocity.y >= 0.0 {
55            return context.velocity;
56        }
57
58        let entity_factor = if context.is_living_entity { 1.0 } else { 0.8 };
59        DVec3::new(
60            context.velocity.x,
61            -context.velocity.y * BED_BOUNCE_SCALE * entity_factor,
62            context.velocity.z,
63        )
64    }
65
66    fn head_state_and_pos(
67        &self,
68        world: &Arc<World>,
69        state: BlockStateId,
70        pos: BlockPos,
71    ) -> Option<(BlockStateId, BlockPos)> {
72        if state.get_value(BED_PART) == BedPart::Head {
73            return Some((state, pos));
74        }
75
76        let head_pos = state.get_value(FACING).relative(pos);
77        let head_state = world.get_block_state(head_pos);
78        (head_state.get_block() == self.block).then_some((head_state, head_pos))
79    }
80
81    const fn neighbor_direction(part: &BedPart, facing: Direction) -> Direction {
82        match part {
83            BedPart::Foot => facing,
84            BedPart::Head => facing.opposite(),
85        }
86    }
87
88    pub(crate) fn find_standup_position(
89        world: &Arc<World>,
90        entity: &dyn Entity,
91        forward_dir: Direction,
92        block_pos: BlockPos,
93    ) -> Option<DVec3> {
94        Self::find_standup_position_with_yaw(
95            world,
96            entity,
97            forward_dir,
98            block_pos,
99            entity.rotation().0,
100        )
101    }
102
103    pub(crate) fn find_standup_position_with_yaw(
104        world: &Arc<World>,
105        entity: &dyn Entity,
106        forward_dir: Direction,
107        block_pos: BlockPos,
108        yaw: f32,
109    ) -> Option<DVec3> {
110        let right = forward_dir.rotate_y_clockwise();
111        let side = if right.is_facing_yaw(yaw) {
112            right.opposite()
113        } else {
114            right
115        };
116
117        if world.get_block_state(block_pos.below()).is_bed() {
118            Self::find_bunk_bed_standup_position(world, entity, forward_dir, side, block_pos)
119        } else {
120            let offsets = Self::bed_standup_offsets(forward_dir, side);
121
122            if let Some(safe_pos) =
123                Self::find_standup_position_at_offset(world, entity, block_pos, &offsets, true)
124            {
125                return Some(safe_pos);
126            }
127
128            Self::find_standup_position_at_offset(world, entity, block_pos, &offsets, false)
129        }
130    }
131
132    fn find_bunk_bed_standup_position(
133        world: &Arc<World>,
134        entity: &dyn Entity,
135        forward_dir: Direction,
136        side_dir: Direction,
137        block_pos: BlockPos,
138    ) -> Option<DVec3> {
139        let offsets = Self::bed_surround_standup_offsets(forward_dir, side_dir);
140        let below = block_pos.below();
141        let above_offsets = Self::bed_above_standup_offsets(forward_dir);
142
143        for check_dangerous in [true, false] {
144            for (pos, offsets) in [
145                (block_pos, offsets.as_slice()),
146                (below, offsets.as_slice()),
147                (block_pos, above_offsets.as_slice()),
148            ] {
149                if let Some(pos) = Self::find_standup_position_at_offset(
150                    world,
151                    entity,
152                    pos,
153                    offsets,
154                    check_dangerous,
155                ) {
156                    return Some(pos);
157                }
158            }
159        }
160
161        None
162    }
163
164    fn find_standup_position_at_offset(
165        world: &Arc<World>,
166        entity: &dyn Entity,
167        pos: BlockPos,
168        offsets: &[(i32, i32)],
169        check_dangerous: bool,
170    ) -> Option<DVec3> {
171        for &(off_x, off_z) in offsets {
172            let offset_pos = BlockPos::new(pos.x() + off_x, pos.y(), pos.z() + off_z);
173            if let Some(position) = dismount_helper::find_safe_dismount_location(
174                world,
175                entity,
176                offset_pos,
177                check_dangerous,
178            ) {
179                return Some(position);
180            }
181        }
182
183        None
184    }
185
186    #[must_use]
187    const fn bed_standup_offsets(forward: Direction, side: Direction) -> [(i32, i32); 12] {
188        let surround = Self::bed_surround_standup_offsets(forward, side);
189        let above = Self::bed_above_standup_offsets(forward);
190
191        [
192            surround[0],
193            surround[1],
194            surround[2],
195            surround[3],
196            surround[4],
197            surround[5],
198            surround[6],
199            surround[7],
200            surround[8],
201            surround[9],
202            above[0],
203            above[1],
204        ]
205    }
206
207    #[must_use]
208    const fn bed_surround_standup_offsets(forward: Direction, side: Direction) -> [(i32, i32); 10] {
209        let (fx, fz) = forward.offset_xz();
210        let (sx, sz) = side.offset_xz();
211
212        [
213            (sx, sz),
214            (sx - fx, sz - fz),
215            (sx - fx * 2, sz - fz * 2),
216            (-fx * 2, -fz * 2),
217            (-sx - fx * 2, -sz - fz * 2),
218            (-sx - fx, -sz - fz),
219            (-sx, -sz),
220            (-sx + fx, -sz + fz),
221            (fx, fz),
222            (sx + fx, sz + fz),
223        ]
224    }
225
226    #[must_use]
227    const fn bed_above_standup_offsets(forward: Direction) -> [(i32, i32); 2] {
228        let (fx, fz) = forward.offset_xz();
229        [(0, 0), (-fx, -fz)]
230    }
231}
232
233impl BlockBehavior for BedBlock {
234    fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
235        let facing = context.horizontal_direction();
236        let head_pos = facing.relative(context.place_pos());
237        let head_state = context.world.get_block_state(head_pos);
238        if !head_state.can_be_replaced(context)
239            || !context.world.is_block_within_world_border(head_pos)
240        {
241            return None;
242        }
243
244        Some(self.block.default_state().set_value(FACING, facing))
245    }
246
247    fn fall_on(
248        &self,
249        state: BlockStateId,
250        world: &Arc<World>,
251        pos: BlockPos,
252        context: EntityFallOnContext<'_>,
253    ) -> Option<EntityFallDamage> {
254        self.default_fall_on(state, world, pos, Self::fall_context(context))
255    }
256
257    fn update_entity_movement_after_fall_on(
258        &self,
259        state: BlockStateId,
260        world: &Arc<World>,
261        pos: BlockPos,
262        context: EntityLandingContext,
263    ) -> DVec3 {
264        if context.suppresses_bounce {
265            return self.default_update_entity_movement_after_fall_on(state, world, pos, context);
266        }
267
268        Self::velocity_after_fall(context)
269    }
270
271    fn is_pathfindable(
272        &self,
273        _state: BlockStateId,
274        _computation_type: PathComputationType,
275    ) -> bool {
276        false
277    }
278
279    fn is_bed(&self) -> bool {
280        true
281    }
282
283    fn player_will_destroy(
284        &self,
285        state: BlockStateId,
286        world: &Arc<World>,
287        pos: BlockPos,
288        player: &Player,
289    ) -> BlockStateId {
290        if !player.has_infinite_materials() || state.get_value(BED_PART) != BedPart::Foot {
291            return state;
292        }
293
294        let facing = state.get_value(FACING);
295        let head_pos = Self::neighbor_direction(&BedPart::Foot, facing).relative(pos);
296        let head_state = world.get_block_state(head_pos);
297        if head_state.get_block() != self.block || head_state.get_value(BED_PART) != BedPart::Head {
298            return state;
299        }
300
301        world.set_block(
302            head_pos,
303            vanilla_blocks::AIR.default_state(),
304            UpdateFlags::UPDATE_ALL | UpdateFlags::UPDATE_SUPPRESS_DROPS,
305        );
306        world.destroy_block_effect(head_pos, u32::from(head_state.0), Some(player.id()));
307        state
308    }
309
310    fn update_shape(
311        &self,
312        state: BlockStateId,
313        _world: &dyn ScheduledTickAccess,
314        _pos: BlockPos,
315        direction: Direction,
316        _neighbor_pos: BlockPos,
317        neighbor_state: BlockStateId,
318    ) -> BlockStateId {
319        let part = state.get_value(BED_PART);
320        let facing = state.get_value(FACING);
321        if direction != Self::neighbor_direction(&part, facing) {
322            return state;
323        }
324
325        if neighbor_state.get_block() == self.block && neighbor_state.get_value(BED_PART) != part {
326            return state.set_value(OCCUPIED, neighbor_state.get_value(OCCUPIED));
327        }
328
329        vanilla_blocks::AIR.default_state()
330    }
331
332    fn set_placed_by(
333        &self,
334        state: BlockStateId,
335        world: &Arc<World>,
336        pos: BlockPos,
337        _source: &PlacementSource<'_>,
338    ) {
339        let facing = state.get_value(FACING);
340        let head_pos = facing.relative(pos);
341        let head_state = state.set_value(BED_PART, BedPart::Head);
342
343        world.set_block(head_pos, head_state, UpdateFlags::UPDATE_ALL);
344        world.update_neighbors_at(pos, &vanilla_blocks::AIR);
345        world.update_neighbor_shapes_at(state, pos, UpdateFlags::UPDATE_ALL, World::UPDATE_LIMIT);
346    }
347
348    fn use_without_item(
349        &self,
350        state: BlockStateId,
351        world: &Arc<World>,
352        pos: BlockPos,
353        player: &Player,
354        _hit_result: &BlockHitResult,
355        _inv: &mut InventoryAccess,
356    ) -> InteractionResult {
357        let Some((head_state, head_pos)) = self.head_state_and_pos(world, state, pos) else {
358            return InteractionResult::Consume;
359        };
360
361        if world.dimension_type.bed_rule.explodes {
362            // TODO: When WOrld::explode foundation exists display the bedrule error remove both halves and create the bad respawn point explosion
363            return InteractionResult::SuccessServer;
364        }
365
366        if head_state.get_value(OCCUPIED) {
367            // TODO: Mirror vanilla `kickVillagerOutOfBed`: find a sleeping
368            // villager in this bed AABB and call `stopSleeping` once villager
369            // sleeping exists.
370            player.send_overlay_message(&TextComponent::translated(TranslatedMessage {
371                key: "block.minecraft.bed.occupied".into(),
372                fallback: None,
373                args: None,
374            }));
375            return InteractionResult::SuccessServer;
376        }
377
378        if let Err(problem) = player.start_sleep_in_bed(head_pos)
379            && let Some(message) = problem.message()
380        {
381            player.send_overlay_message(message);
382        }
383
384        InteractionResult::SuccessServer
385    }
386}
387
388#[cfg(test)]
389mod tests {
390    use super::*;
391
392    use steel_registry::{sound_events, vanilla_entities};
393
394    use crate::behavior::EntityFallOnFacts;
395
396    fn landing(
397        velocity: DVec3,
398        is_living_entity: bool,
399        suppresses_bounce: bool,
400    ) -> EntityLandingContext {
401        EntityLandingContext::new(velocity, is_living_entity, suppresses_bounce)
402    }
403
404    #[test]
405    fn bed_halves_fall_distance_before_default_damage() {
406        let context = BedBlock::fall_context(EntityFallOnContext::new(
407            12.0,
408            false,
409            EntityFallOnFacts::new(
410                &vanilla_entities::PLAYER,
411                true,
412                0.6,
413                1.8,
414                (
415                    &sound_events::ENTITY_PLAYER_SMALL_FALL,
416                    &sound_events::ENTITY_PLAYER_BIG_FALL,
417                ),
418            ),
419            None,
420        ));
421
422        assert!((context.fall_distance - 6.0).abs() < f64::EPSILON);
423        assert!(!context.suppresses_bounce);
424        assert!(context.entity.is_player());
425    }
426
427    #[test]
428    fn living_entities_bounce_with_bed_factor() {
429        let velocity =
430            BedBlock::velocity_after_fall(landing(DVec3::new(1.0, -3.0, -2.0), true, false));
431
432        assert!((velocity.y - 1.980_000_078_678_131).abs() < f64::EPSILON);
433        assert!((velocity.x - 1.0).abs() < f64::EPSILON);
434        assert!((velocity.z + 2.0).abs() < f64::EPSILON);
435    }
436
437    #[test]
438    fn non_living_entities_bounce_with_vanilla_reduction() {
439        let velocity =
440            BedBlock::velocity_after_fall(landing(DVec3::new(1.0, -3.0, -2.0), false, false));
441
442        assert!((velocity.y - 1.584_000_062_942_505).abs() < f64::EPSILON);
443    }
444
445    #[test]
446    fn upward_velocity_is_not_changed_by_bounce_logic() {
447        let velocity =
448            BedBlock::velocity_after_fall(landing(DVec3::new(1.0, 0.5, -2.0), true, false));
449
450        assert_eq!(velocity, DVec3::new(1.0, 0.5, -2.0));
451    }
452}