Skip to main content

steel_core/behavior/blocks/redstone/
bell_block.rs

1//! Vanilla bell behavior.
2
3use 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/// Vanilla `BellBlock`, including placement, ringing, and redstone activation.
39#[block_behavior]
40pub struct BellBlock {
41    block: BlockRef,
42}
43
44impl BellBlock {
45    /// Creates bell behavior for `block`.
46    #[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    // TODO: Notify villagers when the villager AI is fully implemented.
71    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}