steel_core/block_entity/entities/
bell.rs1use 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
33pub 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}
48unsafe impl DowncastType for BellBlockEntity {
50 const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:block_entity/bell");
51}
52
53impl BellBlockEntity {
54 #[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 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 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}