1use std::sync::{Arc, Weak};
4
5use steel_macros::block_behavior;
6use steel_protocol::packets::game::SoundSource;
7use steel_registry::block_entity_type::BlockEntityTypeRef;
8use steel_registry::blocks::BlockRef;
9use steel_registry::blocks::block_state_ext::BlockStateExt as _;
10use steel_registry::blocks::properties::{
11 BellAttachType, BlockStateProperties, BoolProperty, Direction, EnumProperty,
12};
13use steel_registry::vanilla_custom_stats::BELL_RING;
14use steel_registry::{
15 sound_events, vanilla_block_entity_types, vanilla_blocks, vanilla_game_events,
16};
17use steel_utils::types::UpdateFlags;
18use steel_utils::{BlockPos, BlockStateId, Downcast as _};
19
20use crate::behavior::{
21 BlockBehavior, BlockEntityCreation, BlockHitResult, BlockPlaceContext, InteractionResult,
22 InventoryAccess,
23};
24use crate::block_entity::entities::BellBlockEntity;
25use crate::block_entity::{BLOCK_ENTITIES, BlockEntityTicker};
26use crate::entity::Entity;
27use crate::entity::ai::path::PathComputationType;
28use crate::entity::projectile::Projectile;
29use crate::player::Player;
30use crate::world::game_event::GameEventContext;
31use crate::world::{ClipHitResult, LevelReader, ScheduledTickAccess, SignalGetter as _, World};
32
33const MAX_HIT_HEIGHT: f64 = 0.8125;
34const FACING: &EnumProperty<Direction> = &BlockStateProperties::FACING;
35const BELL_ATTACHMENT: &EnumProperty<BellAttachType> = &BlockStateProperties::BELL_ATTACHMENT;
36const POWERED: &BoolProperty = &BlockStateProperties::POWERED;
37
38#[block_behavior]
40pub struct BellBlock {
41 block: BlockRef,
42}
43
44impl BellBlock {
45 #[must_use]
47 pub const fn new(block: BlockRef) -> Self {
48 Self { block }
49 }
50
51 fn connected_direction(state: BlockStateId) -> Direction {
52 match state.get_value(BELL_ATTACHMENT) {
53 BellAttachType::Floor => Direction::Down,
54 BellAttachType::Ceiling => Direction::Up,
55 BellAttachType::SingleWall | BellAttachType::DoubleWall => state.get_value(FACING),
56 }
57 }
58
59 fn has_support(world: &dyn LevelReader, pos: BlockPos, direction: Direction) -> bool {
60 let support_pos = pos.relative(direction);
61 let support = world.get_block_state(support_pos);
62
63 world.is_face_sturdy(support, support_pos, direction.opposite())
64 }
65
66 fn ring(source: Option<&dyn Entity>, world: &Arc<World>, pos: BlockPos, direction: Direction) {
68 let Some(block_entity) = world.get_block_entity(pos) else {
69 return;
70 };
71 let Some(bell) = block_entity.downcast_ref::<BellBlockEntity>() else {
72 return;
73 };
74 bell.on_hit(direction);
75
76 world.play_sound(
77 &sound_events::BLOCK_BELL_USE,
78 SoundSource::Blocks,
79 pos,
80 2.0,
81 1.0,
82 None,
83 );
84 world.game_event(
85 &vanilla_game_events::BLOCK_CHANGE,
86 pos,
87 &GameEventContext::new(source, None),
88 );
89 if let Some(entity) = source
90 && let Some(player) = entity.as_player()
91 {
92 player.award_custom_stat(&BELL_RING);
93 }
94 }
95
96 fn is_proper_hit(state: BlockStateId, direction: Direction, height: f64) -> bool {
97 if direction.axis().is_vertical() {
98 return false;
99 }
100
101 if height > MAX_HIT_HEIGHT {
102 return false;
103 }
104
105 let facing = state.get_value(FACING);
106 let attachment = state.get_value(BELL_ATTACHMENT);
107
108 match attachment {
109 BellAttachType::Floor => facing.axis() == direction.axis(),
110 BellAttachType::Ceiling => true,
111 BellAttachType::SingleWall | BellAttachType::DoubleWall => {
112 facing.axis() != direction.axis()
113 }
114 }
115 }
116
117 fn update_powered(state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
118 let powered = world.has_neighbor_signal(pos);
119 let old_powered = state.get_value(POWERED);
120
121 if powered == old_powered {
122 return;
123 }
124
125 if powered {
126 let direction = state.get_value(FACING);
127 Self::ring(None, world, pos, direction);
128 }
129
130 let new_state = state.set_value(POWERED, powered);
131 world.set_block(pos, new_state, UpdateFlags::UPDATE_ALL);
132 }
133}
134
135impl BlockBehavior for BellBlock {
136 fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
137 let pos = context.place_pos();
138 let clicked_face = context.clicked_face();
139 let player_facing = context.horizontal_direction();
140
141 let mut state = self.block.default_state();
142 state = state.set_value(FACING, player_facing);
143
144 if clicked_face == Direction::Up {
145 state = state.set_value(BELL_ATTACHMENT, BellAttachType::Floor);
146 } else if clicked_face == Direction::Down {
147 state = state.set_value(BELL_ATTACHMENT, BellAttachType::Ceiling);
148 } else {
149 let wall_facing = clicked_face.opposite();
150
151 state = state.set_value(FACING, wall_facing);
152
153 let opposite = wall_facing.opposite();
154
155 let double_wall = Self::has_support(context.world, pos, wall_facing)
156 && Self::has_support(context.world, pos, opposite);
157
158 let attachment = if double_wall {
159 BellAttachType::DoubleWall
160 } else {
161 BellAttachType::SingleWall
162 };
163
164 state = state.set_value(BELL_ATTACHMENT, attachment);
165
166 if self.can_survive(state, context.world, pos) {
167 return Some(state);
168 }
169
170 let can_attach_below = Self::has_support(context.world, pos, Direction::Down);
171 let fallback = if can_attach_below {
172 BellAttachType::Floor
173 } else {
174 BellAttachType::Ceiling
175 };
176 state = state.set_value(BELL_ATTACHMENT, fallback);
177 }
178
179 self.can_survive(state, context.world, pos).then_some(state)
180 }
181
182 fn can_survive(&self, state: BlockStateId, world: &dyn LevelReader, pos: BlockPos) -> bool {
183 let direction = Self::connected_direction(state);
184 Self::has_support(world, pos, direction)
185 }
186
187 fn update_shape(
188 &self,
189 state: BlockStateId,
190 world: &dyn ScheduledTickAccess,
191 pos: BlockPos,
192 direction: Direction,
193 neighbor_pos: BlockPos,
194 neighbor_state: BlockStateId,
195 ) -> BlockStateId {
196 let attachment = state.get_value(BELL_ATTACHMENT);
197 let support_direction = Self::connected_direction(state);
198
199 if support_direction == direction
200 && attachment != BellAttachType::DoubleWall
201 && !Self::has_support(world, pos, support_direction)
202 {
203 return vanilla_blocks::AIR.default_state();
204 }
205
206 let facing = state.get_value(FACING);
207
208 if direction.axis() == facing.axis() {
209 if attachment == BellAttachType::DoubleWall
210 && !world.is_face_sturdy(neighbor_state, neighbor_pos, direction)
211 {
212 return state
213 .set_value(FACING, direction.opposite())
214 .set_value(BELL_ATTACHMENT, BellAttachType::SingleWall);
215 }
216
217 if attachment == BellAttachType::SingleWall
218 && support_direction.opposite() == direction
219 && world.is_face_sturdy(neighbor_state, neighbor_pos, facing)
220 {
221 return state.set_value(BELL_ATTACHMENT, BellAttachType::DoubleWall);
222 }
223 }
224
225 state
226 }
227
228 fn handle_neighbor_changed(
229 &self,
230 state: BlockStateId,
231 world: &Arc<World>,
232 pos: BlockPos,
233 _source_block: BlockRef,
234 _moved_by_piston: bool,
235 ) {
236 Self::update_powered(state, world, pos);
237 }
238
239 fn use_without_item(
240 &self,
241 state: BlockStateId,
242 world: &Arc<World>,
243 pos: BlockPos,
244 player: &Player,
245 hit: &BlockHitResult,
246 _inv: &mut InventoryAccess,
247 ) -> InteractionResult {
248 let height = hit.location.y - f64::from(pos.y());
249 if !Self::is_proper_hit(state, hit.direction, height) {
250 return InteractionResult::Pass;
251 }
252
253 Self::ring(Some(player), world, pos, hit.direction);
254 InteractionResult::Success
255 }
256
257 fn on_projectile_hit(
258 &self,
259 state: BlockStateId,
260 world: &Arc<World>,
261 hit: &ClipHitResult,
262 projectile: &dyn Projectile,
263 ) {
264 let height = hit.location.y - f64::from(hit.block_pos.y());
265 if !Self::is_proper_hit(state, hit.direction, height) {
266 return;
267 }
268
269 let owner = projectile.get_owner();
270 let source = owner
271 .as_deref()
272 .filter(|entity| entity.as_player().is_some());
273 Self::ring(source, world, hit.block_pos, hit.direction);
274 }
275
276 fn new_block_entity(
277 &self,
278 level: Weak<World>,
279 pos: BlockPos,
280 state: BlockStateId,
281 ) -> BlockEntityCreation {
282 BlockEntityCreation::from_registered_factory(BLOCK_ENTITIES.create(
283 &vanilla_block_entity_types::BELL,
284 level,
285 pos,
286 state,
287 ))
288 }
289
290 fn get_block_entity_ticker(
291 &self,
292 _world: &Arc<World>,
293 _state: BlockStateId,
294 block_entity_type: BlockEntityTypeRef,
295 ) -> Option<BlockEntityTicker> {
296 BlockEntityTicker::for_matching_entity_tick(
297 block_entity_type,
298 &vanilla_block_entity_types::BELL,
299 )
300 }
301
302 fn trigger_event(
303 &self,
304 _state: BlockStateId,
305 world: &Arc<World>,
306 pos: BlockPos,
307 event: i32,
308 data: i32,
309 ) -> bool {
310 let Some(block_entity) = world.get_block_entity(pos) else {
311 return false;
312 };
313 block_entity.trigger_event(event, data)
314 }
315
316 fn is_pathfindable(
317 &self,
318 _state: BlockStateId,
319 _computation_type: PathComputationType,
320 ) -> bool {
321 false
322 }
323}
324
325#[cfg(test)]
326mod tests {
327 use steel_registry::init_vanilla_registry;
328 use steel_utils::ChunkPos;
329
330 use super::*;
331 use crate::behavior::{BLOCK_BEHAVIORS, init_behaviors};
332 use crate::block_entity::init_block_entities;
333 use crate::test_support::{fresh_test_world, insert_ready_full_chunk};
334
335 #[test]
336 fn wall_attachment_uses_facing_as_support_direction() {
337 init_vanilla_registry();
338 let state = vanilla_blocks::BELL
339 .default_state()
340 .set_value(BELL_ATTACHMENT, BellAttachType::SingleWall)
341 .set_value(FACING, Direction::West);
342
343 assert_eq!(BellBlock::connected_direction(state), Direction::West);
344 }
345
346 #[test]
347 fn generated_registry_bell_behavior_creates_registered_typed_entity() {
348 init_vanilla_registry();
349 init_block_entities();
350 init_behaviors();
351 let behavior = BLOCK_BEHAVIORS.get_behavior(&vanilla_blocks::BELL);
352 let entity = behavior
353 .new_block_entity(
354 Weak::new(),
355 BlockPos::new(0, 64, 0),
356 vanilla_blocks::BELL.default_state(),
357 )
358 .into_created()
359 .expect("bell should create its registered block entity");
360
361 assert!(BLOCK_ENTITIES.has_factory(&vanilla_block_entity_types::BELL));
362 assert!(entity.downcast_ref::<BellBlockEntity>().is_some());
363 }
364
365 #[test]
366 fn placed_bell_is_stored_with_its_ticker() {
367 init_vanilla_registry();
368 init_block_entities();
369 init_behaviors();
370 let world = fresh_test_world("placed_bell_entity");
371 let pos = BlockPos::new(4, 64, 4);
372 insert_ready_full_chunk(&world, ChunkPos::from_block_pos(pos));
373 assert!(world.set_block(
374 pos.relative(Direction::Down),
375 vanilla_blocks::STONE.default_state(),
376 UpdateFlags::UPDATE_ALL,
377 ));
378 let state = vanilla_blocks::BELL.default_state();
379 assert!(world.set_block(pos, state, UpdateFlags::UPDATE_ALL));
380
381 let entity = world
382 .get_block_entity(pos)
383 .expect("placed bell should be stored as a block entity");
384 assert!(entity.downcast_ref::<BellBlockEntity>().is_some());
385 assert!(
386 BLOCK_BEHAVIORS
387 .get_behavior(&vanilla_blocks::BELL)
388 .get_block_entity_ticker(&world, state, entity.get_type())
389 .is_some()
390 );
391 }
392}