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#[block_behavior]
36pub struct BedBlock {
37 block: BlockRef,
38}
39
40impl BedBlock {
41 #[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 return InteractionResult::SuccessServer;
364 }
365
366 if head_state.get_value(OCCUPIED) {
367 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}