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(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    // TODO: Notify villagers when the villager AI is fully implemented.
67    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}