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