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        InteractionResult, InventoryAccess, PlacementSource,
7    },
8    entity::{Entity, ai::path::PathComputationType, damage::DamageSource, dismount_helper},
9    player::Player,
10    world::{ExplosionInteraction, ExplosionOptions, LevelReader, ScheduledTickAccess, World},
11};
12use glam::DVec3;
13use steel_macros::block_behavior;
14use steel_registry::blocks::properties::{BedPart, BoolProperty, EnumProperty};
15use steel_registry::blocks::{
16    BlockRef, block_state_ext::BlockStateExt, properties::BlockStateProperties,
17};
18use steel_registry::{vanilla_blocks, vanilla_custom_stats, vanilla_damage_types};
19use steel_utils::{
20    BlockPos, BlockStateId, Direction, axis::Axis, translations, types::UpdateFlags,
21};
22
23const BED_PART: &EnumProperty<BedPart> = &BlockStateProperties::BED_PART;
24const FACING: &EnumProperty<Direction> = &BlockStateProperties::HORIZONTAL_FACING;
25const OCCUPIED: &BoolProperty = &BlockStateProperties::OCCUPIED;
26const EXPLOSION_RADIUS: f32 = 5.0;
27
28/// Behavior for beds
29///
30/// TODO: Mirror vanilla `BedBlock.kickVillagerOutOfBed` once villager sleeping
31/// entities exist.
32#[block_behavior]
33pub struct BedBlock {
34    pub(super) block: BlockRef,
35}
36
37impl BedBlock {
38    /// Creates a bed block behavior.
39    #[must_use]
40    pub const fn new(block: BlockRef) -> Self {
41        Self { block }
42    }
43
44    pub(super) fn head_state_and_pos(
45        &self,
46        world: &Arc<World>,
47        state: BlockStateId,
48        pos: BlockPos,
49    ) -> Option<(BlockStateId, BlockPos)> {
50        let part = state.get_value(BED_PART);
51        let other_pos = Self::neighbor_direction(part, state.get_value(FACING)).relative(pos);
52        let other_state = world.get_block_state(other_pos);
53        if other_state.get_block() != self.block || other_state.get_value(BED_PART) == part {
54            return None;
55        }
56
57        match part {
58            BedPart::Head => Some((state, pos)),
59            BedPart::Foot => Some((other_state, other_pos)),
60        }
61    }
62
63    /// Removes both halves and explodes at the head, used when the bed rule destroys beds on use.
64    fn destroy_on_use(&self, head_state: BlockStateId, world: &Arc<World>, head_pos: BlockPos) {
65        world.remove_block(head_pos, false);
66        let foot_pos = head_state.get_value(FACING).opposite().relative(head_pos);
67        if world.get_block_state(foot_pos).get_block() == self.block {
68            world.remove_block(foot_pos, false);
69        }
70
71        let (x, y, z) = head_pos.get_center();
72        let center = DVec3::new(x, y, z);
73        let mut options =
74            ExplosionOptions::new(center, EXPLOSION_RADIUS, ExplosionInteraction::Block);
75        options.damage_source = Some(
76            DamageSource::environment(&vanilla_damage_types::BAD_RESPAWN_POINT)
77                .with_source_position(center),
78        );
79        options.fire = true;
80        world.explode(options);
81    }
82
83    const fn neighbor_direction(part: BedPart, facing: Direction) -> Direction {
84        match part {
85            BedPart::Foot => facing,
86            BedPart::Head => facing.opposite(),
87        }
88    }
89
90    pub(crate) fn find_standup_position(
91        world: &Arc<World>,
92        entity: &dyn Entity,
93        forward_dir: Direction,
94        block_pos: BlockPos,
95    ) -> Option<DVec3> {
96        Self::find_standup_position_with_yaw(
97            world,
98            entity,
99            forward_dir,
100            block_pos,
101            entity.rotation().0,
102        )
103    }
104
105    pub(crate) fn find_standup_position_with_yaw(
106        world: &Arc<World>,
107        entity: &dyn Entity,
108        forward_dir: Direction,
109        block_pos: BlockPos,
110        yaw: f32,
111    ) -> Option<DVec3> {
112        let right = forward_dir.rotate_y_clockwise();
113        let side = if right.is_facing_yaw(yaw) {
114            right.opposite()
115        } else {
116            right
117        };
118
119        if world.get_block_state(block_pos.below()).is_bed() {
120            Self::find_bunk_bed_standup_position(world, entity, forward_dir, side, block_pos)
121        } else {
122            let offsets = Self::bed_standup_offsets(forward_dir, side);
123
124            if let Some(safe_pos) =
125                Self::find_standup_position_at_offset(world, entity, block_pos, &offsets, true)
126            {
127                return Some(safe_pos);
128            }
129
130            Self::find_standup_position_at_offset(world, entity, block_pos, &offsets, false)
131        }
132    }
133
134    fn find_bunk_bed_standup_position(
135        world: &Arc<World>,
136        entity: &dyn Entity,
137        forward_dir: Direction,
138        side_dir: Direction,
139        block_pos: BlockPos,
140    ) -> Option<DVec3> {
141        let offsets = Self::bed_surround_standup_offsets(forward_dir, side_dir);
142        let below = block_pos.below();
143        let above_offsets = Self::bed_above_standup_offsets(forward_dir);
144
145        for check_dangerous in [true, false] {
146            for (pos, offsets) in [
147                (block_pos, offsets.as_slice()),
148                (below, offsets.as_slice()),
149                (block_pos, above_offsets.as_slice()),
150            ] {
151                if let Some(pos) = Self::find_standup_position_at_offset(
152                    world,
153                    entity,
154                    pos,
155                    offsets,
156                    check_dangerous,
157                ) {
158                    return Some(pos);
159                }
160            }
161        }
162
163        None
164    }
165
166    fn find_standup_position_at_offset(
167        world: &Arc<World>,
168        entity: &dyn Entity,
169        pos: BlockPos,
170        offsets: &[(i32, i32)],
171        check_dangerous: bool,
172    ) -> Option<DVec3> {
173        for &(off_x, off_z) in offsets {
174            let offset_pos = BlockPos::new(pos.x() + off_x, pos.y(), pos.z() + off_z);
175            if let Some(position) = dismount_helper::find_safe_dismount_location(
176                world,
177                entity,
178                offset_pos,
179                check_dangerous,
180            ) {
181                return Some(position);
182            }
183        }
184
185        None
186    }
187
188    #[must_use]
189    const fn bed_standup_offsets(forward: Direction, side: Direction) -> [(i32, i32); 12] {
190        let surround = Self::bed_surround_standup_offsets(forward, side);
191        let above = Self::bed_above_standup_offsets(forward);
192
193        [
194            surround[0],
195            surround[1],
196            surround[2],
197            surround[3],
198            surround[4],
199            surround[5],
200            surround[6],
201            surround[7],
202            surround[8],
203            surround[9],
204            above[0],
205            above[1],
206        ]
207    }
208
209    #[must_use]
210    const fn bed_surround_standup_offsets(forward: Direction, side: Direction) -> [(i32, i32); 10] {
211        let (fx, fz) = forward.offset_xz();
212        let (sx, sz) = side.offset_xz();
213
214        [
215            (sx, sz),
216            (sx - fx, sz - fz),
217            (sx - fx * 2, sz - fz * 2),
218            (-fx * 2, -fz * 2),
219            (-sx - fx * 2, -sz - fz * 2),
220            (-sx - fx, -sz - fz),
221            (-sx, -sz),
222            (-sx + fx, -sz + fz),
223            (fx, fz),
224            (sx + fx, sz + fz),
225        ]
226    }
227
228    #[must_use]
229    const fn bed_above_standup_offsets(forward: Direction) -> [(i32, i32); 2] {
230        let (fx, fz) = forward.offset_xz();
231        [(0, 0), (-fx, -fz)]
232    }
233}
234
235impl BlockBehavior for BedBlock {
236    fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
237        let facing = context.horizontal_direction();
238        let head_pos = facing.relative(context.place_pos());
239        let head_state = context.world.get_block_state(head_pos);
240        if !head_state.can_be_replaced(context)
241            || !context.world.is_block_within_world_border(head_pos)
242        {
243            return None;
244        }
245
246        Some(self.block.default_state().set_value(FACING, facing))
247    }
248
249    fn is_pathfindable(
250        &self,
251        _state: BlockStateId,
252        _computation_type: PathComputationType,
253    ) -> bool {
254        false
255    }
256
257    fn is_bed(&self) -> bool {
258        true
259    }
260
261    fn get_sleep_height(
262        &self,
263        state: BlockStateId,
264        _world: &dyn LevelReader,
265        _pos: BlockPos,
266    ) -> Option<f64> {
267        let shape = state.get_static_outline_shape();
268        (!shape.is_empty()).then(|| shape.max(Axis::Y))
269    }
270
271    fn player_will_destroy(
272        &self,
273        state: BlockStateId,
274        world: &Arc<World>,
275        pos: BlockPos,
276        player: &Player,
277    ) -> BlockStateId {
278        if !player.has_infinite_materials() || state.get_value(BED_PART) != BedPart::Foot {
279            return state;
280        }
281
282        let facing = state.get_value(FACING);
283        let head_pos = Self::neighbor_direction(BedPart::Foot, facing).relative(pos);
284        let head_state = world.get_block_state(head_pos);
285        if head_state.get_block() != self.block || head_state.get_value(BED_PART) != BedPart::Head {
286            return state;
287        }
288
289        world.set_block(
290            head_pos,
291            vanilla_blocks::AIR.default_state(),
292            UpdateFlags::UPDATE_ALL | UpdateFlags::UPDATE_SUPPRESS_DROPS,
293        );
294        world.destroy_block_effect(head_pos, u32::from(head_state.0), Some(player.id()));
295        state
296    }
297
298    fn update_shape(
299        &self,
300        state: BlockStateId,
301        _world: &dyn ScheduledTickAccess,
302        _pos: BlockPos,
303        direction: Direction,
304        _neighbor_pos: BlockPos,
305        neighbor_state: BlockStateId,
306    ) -> BlockStateId {
307        let part = state.get_value(BED_PART);
308        let facing = state.get_value(FACING);
309        if direction != Self::neighbor_direction(part, facing) {
310            return state;
311        }
312
313        if neighbor_state.get_block() == self.block && neighbor_state.get_value(BED_PART) != part {
314            return state.set_value(OCCUPIED, neighbor_state.get_value(OCCUPIED));
315        }
316
317        vanilla_blocks::AIR.default_state()
318    }
319
320    fn set_placed_by(
321        &self,
322        state: BlockStateId,
323        world: &Arc<World>,
324        pos: BlockPos,
325        _source: &PlacementSource<'_>,
326    ) {
327        let facing = state.get_value(FACING);
328        let head_pos = facing.relative(pos);
329        let head_state = state.set_value(BED_PART, BedPart::Head);
330
331        world.set_block(head_pos, head_state, UpdateFlags::UPDATE_ALL);
332    }
333
334    fn use_without_item(
335        &self,
336        state: BlockStateId,
337        world: &Arc<World>,
338        pos: BlockPos,
339        player: &Player,
340        _hit_result: &BlockHitResult,
341        _inv: &mut InventoryAccess,
342    ) -> InteractionResult {
343        let Some((head_state, head_pos)) = self.head_state_and_pos(world, state, pos) else {
344            return InteractionResult::Consume;
345        };
346
347        let bed_rule = &world.dimension_type.bed_rule;
348        if bed_rule.destroy_on_use {
349            if let Some(message) = bed_rule.error_message {
350                player.send_overlay_message(&message());
351            }
352            self.destroy_on_use(head_state, world, head_pos);
353            return InteractionResult::SuccessServer;
354        }
355
356        if head_state.get_value(OCCUPIED) {
357            // TODO: Mirror vanilla `kickVillagerOutOfBed`: find a sleeping
358            // villager in this bed AABB and call `stopSleeping` once villager
359            // sleeping exists.
360            player.send_overlay_message(
361                &translations::BLOCK_MINECRAFT_BED_OCCUPIED.msg().component(),
362            );
363            return InteractionResult::SuccessServer;
364        }
365
366        if let Err(problem) =
367            player.start_sleep_in_bed(head_pos, bed_rule, &vanilla_custom_stats::SLEEP_IN_BED)
368            && let Some(message) = problem.message()
369        {
370            player.send_overlay_message(message);
371        }
372
373        InteractionResult::SuccessServer
374    }
375}