Skip to main content

steel_core/block_entity/entities/
bell.rs

1//! Bell Block entity behavior.
2
3use std::sync::{Arc, Weak};
4
5use glam::DVec3;
6use simdnbt::borrow::BaseNbtCompound as BorrowedNbtCompound;
7use simdnbt::owned::NbtCompound;
8use steel_protocol::packets::game::SoundSource;
9use steel_registry::blocks::block_state_ext::BlockStateExt as _;
10use steel_registry::blocks::properties::Direction;
11use steel_registry::{
12    REGISTRY, TaggedRegistryExt as _, sound_events, vanilla_block_entity_types,
13    vanilla_entity_type_tags::EntityTypeTag, vanilla_mob_effects,
14};
15use steel_utils::{
16    BlockPos, BlockStateId, DowncastType, DowncastTypeKey, WorldAabb, locks::SyncMutex,
17};
18
19use crate::block_entity::{BlockEntity, BlockEntityBase};
20use crate::entity::{MobEffectInstance, SharedEntity};
21use crate::world::World;
22
23const RING_EVENT_ID: i32 = 1;
24const RING_DURATION: i32 = 50;
25const GLOW_DURATION: i32 = 60;
26const ENTITY_SEARCH_INTERVAL: i64 = 60;
27const RESONATION_DURATION: i32 = 40;
28const RESONATION_DELAY: i32 = 5;
29const SEARCH_RADIUS: f64 = 48.0;
30const HEAR_BELL_RADIUS: f64 = 32.0;
31const GLOW_RADIUS: f64 = 48.0;
32
33/// Stores the transient state and entity reactions for a ringing bell.
34pub struct BellBlockEntity {
35    base: BlockEntityBase,
36    state: SyncMutex<BellState>,
37}
38
39struct BellState {
40    last_ring_timestamp: i64,
41    ticks: i32,
42    shaking: bool,
43    click_direction: Option<Direction>,
44    nearby_entities: Option<Vec<SharedEntity>>,
45    resonating: bool,
46    resonation_ticks: i32,
47}
48// SAFETY: This key uniquely identifies `BellBlockEntity`.
49unsafe impl DowncastType for BellBlockEntity {
50    const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:block_entity/bell");
51}
52
53impl BellBlockEntity {
54    /// Creates a bell block entity at `pos` with the supplied block state.
55    #[must_use]
56    pub fn new(world: Weak<World>, pos: BlockPos, state: BlockStateId) -> Self {
57        Self {
58            base: BlockEntityBase::new(&vanilla_block_entity_types::BELL, world, pos, state),
59            state: SyncMutex::new(BellState {
60                last_ring_timestamp: 0,
61                ticks: 0,
62                shaking: false,
63                click_direction: None,
64                nearby_entities: None,
65                resonating: false,
66                resonation_ticks: 0,
67            }),
68        }
69    }
70
71    /// Starts the bell animation and broadcasts its ring event.
72    pub fn on_hit(&self, direction: Direction) {
73        {
74            let mut state = self.state.lock();
75            state.click_direction = Some(direction);
76            if state.shaking {
77                state.ticks = 0;
78            } else {
79                state.shaking = true;
80            }
81        }
82
83        let Some(world) = self.get_level() else {
84            return;
85        };
86        world.block_event(
87            self.get_block_pos(),
88            self.get_block_state().get_block(),
89            RING_EVENT_ID,
90            direction.get_3d_data_value(),
91        );
92    }
93
94    fn refresh_nearby_entities(&self, world: &World) {
95        let game_time = world.game_time();
96        let should_search = {
97            let state = self.state.lock();
98            state.nearby_entities.is_none()
99                || game_time > state.last_ring_timestamp + ENTITY_SEARCH_INTERVAL
100        };
101
102        if should_search {
103            let pos = self.get_block_pos();
104            let bounds = WorldAabb::new(
105                f64::from(pos.x()),
106                f64::from(pos.y()),
107                f64::from(pos.z()),
108                f64::from(pos.x() + 1),
109                f64::from(pos.y() + 1),
110                f64::from(pos.z() + 1),
111            )
112            .inflate(SEARCH_RADIUS);
113            let entities = world
114                .get_entities_in_aabb(&bounds)
115                .into_iter()
116                .filter(|entity| entity.as_living_entity().is_some())
117                .collect();
118
119            let mut state = self.state.lock();
120            state.nearby_entities = Some(entities);
121            state.last_ring_timestamp = game_time;
122        }
123    }
124
125    fn is_raider_near(entity: &SharedEntity, pos: BlockPos, radius: f64) -> bool {
126        if !entity.is_alive() || entity.is_removed() {
127            return false;
128        }
129        if !REGISTRY
130            .entity_types
131            .is_in_tag(entity.entity_type(), &EntityTypeTag::RAIDERS)
132        {
133            return false;
134        }
135
136        let center = DVec3::new(
137            f64::from(pos.x()) + 0.5,
138            f64::from(pos.y()) + 0.5,
139            f64::from(pos.z()) + 0.5,
140        );
141        entity.position().distance_squared(center) < radius * radius
142    }
143
144    fn has_nearby_raider(entities: Option<&[SharedEntity]>, pos: BlockPos) -> bool {
145        entities.is_some_and(|entities| {
146            entities
147                .iter()
148                .any(|entity| Self::is_raider_near(entity, pos, HEAR_BELL_RADIUS))
149        })
150    }
151
152    fn glow_nearby_raiders(entities: Option<&[SharedEntity]>, pos: BlockPos) {
153        let Some(entities) = entities else {
154            return;
155        };
156
157        for entity in entities {
158            if !Self::is_raider_near(entity, pos, GLOW_RADIUS) {
159                continue;
160            }
161            let Some(living) = entity.as_living_entity() else {
162                continue;
163            };
164            living.add_mob_effect(MobEffectInstance::with_duration(
165                vanilla_mob_effects::GLOWING,
166                GLOW_DURATION,
167                0,
168            ));
169        }
170    }
171}
172
173impl BlockEntity for BellBlockEntity {
174    fn base(&self) -> &BlockEntityBase {
175        &self.base
176    }
177
178    fn load_additional(&self, _nbt: &BorrowedNbtCompound<'_>) {}
179
180    fn save_additional(&self, _nbt: &mut NbtCompound) {}
181
182    fn trigger_event(&self, event: i32, data: i32) -> bool {
183        if event != RING_EVENT_ID {
184            return false;
185        }
186
187        let Some(world) = self.get_level() else {
188            return false;
189        };
190        self.refresh_nearby_entities(&world);
191
192        let mut state = self.state.lock();
193        state.resonation_ticks = 0;
194        state.click_direction = Some(Direction::from_3d_data_value(data));
195        state.ticks = 0;
196        state.shaking = true;
197        true
198    }
199    // TODO: make bell sleep when not used
200    fn tick(&self, world: &Arc<World>) {
201        let pos = self.get_block_pos();
202        let mut state = self.state.lock();
203
204        if state.shaking {
205            state.ticks += 1;
206        }
207        if state.ticks >= RING_DURATION {
208            state.shaking = false;
209            state.ticks = 0;
210        }
211
212        if state.ticks >= RESONATION_DELAY
213            && state.resonation_ticks == 0
214            && Self::has_nearby_raider(state.nearby_entities.as_deref(), pos)
215        {
216            state.resonating = true;
217            world.play_sound(
218                &sound_events::BLOCK_BELL_RESONATE,
219                SoundSource::Blocks,
220                pos,
221                1.0,
222                1.0,
223                None,
224            );
225        }
226
227        if !state.resonating {
228            return;
229        }
230        if state.resonation_ticks < RESONATION_DURATION {
231            state.resonation_ticks += 1;
232            return;
233        }
234
235        Self::glow_nearby_raiders(state.nearby_entities.as_deref(), pos);
236        state.resonating = false;
237    }
238}
239
240#[cfg(test)]
241mod tests {
242    use steel_registry::blocks::properties::Direction;
243    use steel_registry::{init_vanilla_registry, vanilla_blocks};
244
245    use super::*;
246    use crate::test_support::fresh_test_world;
247
248    #[test]
249    fn ring_event_starts_shaking_in_the_supplied_direction() {
250        init_vanilla_registry();
251        let world = fresh_test_world("bell_ring_event");
252        let bell = BellBlockEntity::new(
253            Arc::downgrade(&world),
254            BlockPos::new(4, 64, 4),
255            vanilla_blocks::BELL.default_state(),
256        );
257
258        assert!(bell.trigger_event(RING_EVENT_ID, Direction::West.get_3d_data_value()));
259
260        let state = bell.state.lock();
261        assert!(state.shaking);
262        assert_eq!(state.ticks, 0);
263        assert_eq!(state.click_direction, Some(Direction::West));
264    }
265
266    #[test]
267    fn unrelated_block_event_is_rejected() {
268        init_vanilla_registry();
269        let world = fresh_test_world("bell_unrelated_event");
270        let bell = BellBlockEntity::new(
271            Arc::downgrade(&world),
272            BlockPos::new(4, 64, 4),
273            vanilla_blocks::BELL.default_state(),
274        );
275
276        assert!(!bell.trigger_event(2, 0));
277        assert!(!bell.state.lock().shaking);
278    }
279}