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#[block_behavior]
33pub struct BedBlock {
34 pub(super) block: BlockRef,
35}
36
37impl BedBlock {
38 #[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 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 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}