steel_core/block_entity/entities/
beacon.rs1use std::{
9 mem,
10 str::FromStr as _,
11 sync::{Arc, Weak},
12};
13
14use glam::DVec3;
15use simdnbt::borrow::{BaseNbtCompound as BorrowedNbtCompound, NbtCompound as NbtCompoundView};
16use simdnbt::owned::NbtCompound;
17use steel_registry::blocks::block_state_ext::BlockStateExt as _;
18use steel_registry::mob_effect::MobEffectRef;
19use steel_registry::sound_event::SoundEventRef;
20use steel_registry::{
21 REGISTRY, RegistryExt, TaggedRegistryExt, sound_events, vanilla_block_entity_types,
22 vanilla_block_tags, vanilla_blocks, vanilla_entities, vanilla_mob_effects,
23};
24use steel_utils::color::ArgbColor;
25use steel_utils::locks::SyncMutex;
26use steel_utils::{
27 BlockPos, BlockStateId, Downcast as _, DowncastType, DowncastTypeKey, Identifier, WorldAabb,
28};
29
30use crate::behavior::BLOCK_BEHAVIORS;
31use crate::block_entity::{BlockEntity, BlockEntityBase};
32use crate::chunk::heightmap::HeightmapType;
33use crate::chunk::light::MAX_LIGHT_LEVEL;
34use crate::entity::{LivingEntity as _, MobEffectInstance};
35use crate::player::Player;
36use crate::world::World;
37
38const MAX_LEVELS: i32 = 4;
39
40const BEACON_TICK_INTERVAL: i64 = 80;
41
42const BLOCKS_CHECK_PER_TICK: i32 = 10;
44
45const BASE_EFFECT_RANGE: f64 = 10.0;
46const EFFECT_RANGE_PER_LEVEL: f64 = 10.0;
47
48pub(crate) const BEACON_EFFECTS: [&[MobEffectRef]; 4] = [
50 &[vanilla_mob_effects::SPEED, vanilla_mob_effects::HASTE],
51 &[
52 vanilla_mob_effects::RESISTANCE,
53 vanilla_mob_effects::JUMP_BOOST,
54 ],
55 &[vanilla_mob_effects::STRENGTH],
56 &[vanilla_mob_effects::REGENERATION],
57];
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub struct BeamSection {
63 color: ArgbColor,
64 height: i32,
65}
66
67impl BeamSection {
68 const fn new(color: ArgbColor) -> Self {
69 Self { color, height: 1 }
70 }
71
72 const fn increase_height(&mut self) {
73 self.height += 1;
74 }
75}
76
77pub struct BeaconState {
79 pub(crate) levels: i32,
80 pub(crate) primary_power: Option<MobEffectRef>,
81 pub(crate) secondary_power: Option<MobEffectRef>,
82 last_check_y: i32,
84 checking_beam_sections: Vec<BeamSection>,
85 pub(crate) beam_sections: Vec<BeamSection>,
87}
88
89impl BeaconState {
90 const fn new() -> Self {
91 Self {
92 levels: 0,
93 primary_power: None,
94 secondary_power: None,
95 last_check_y: i32::MIN,
96 checking_beam_sections: Vec::new(),
97 beam_sections: Vec::new(),
98 }
99 }
100
101 pub(crate) fn filter_effect(effect: Option<MobEffectRef>) -> Option<MobEffectRef> {
102 effect.filter(|effect| {
103 BEACON_EFFECTS
104 .iter()
105 .copied()
106 .flatten()
107 .any(|valid| valid.key == effect.key)
108 })
109 }
110
111 pub(crate) fn validate_effects(
114 primary: Option<MobEffectRef>,
115 secondary: Option<MobEffectRef>,
116 levels: i32,
117 ) -> bool {
118 if secondary.is_some() && levels < MAX_LEVELS {
119 return false;
120 }
121 let primary_level = Self::required_levels_for(primary);
122 let secondary_level = Self::required_levels_for(secondary);
123 if primary_level > levels || secondary_level > levels {
124 return false;
125 }
126 if primary_level >= MAX_LEVELS {
127 return false;
128 }
129 secondary_level == 0
130 || secondary_level >= MAX_LEVELS
131 || primary.zip(secondary).is_some_and(|(p, s)| p.key == s.key)
132 }
133
134 fn required_levels_for(effect: Option<MobEffectRef>) -> i32 {
137 let Some(effect) = effect else {
138 return 0;
139 };
140 for (i, tier) in BEACON_EFFECTS.iter().enumerate() {
141 if tier.iter().any(|e| e.key == effect.key) {
142 return i as i32 + 1;
143 }
144 }
145 i32::MAX
146 }
147}
148
149pub struct BeaconBlockEntity {
151 base: Arc<BlockEntityBase>,
152 state: Arc<SyncMutex<BeaconState>>,
153}
154
155unsafe impl DowncastType for BeaconBlockEntity {
157 const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:block_entity/beacon");
158}
159
160impl BeaconBlockEntity {
161 #[must_use]
163 pub fn new(level: Weak<World>, pos: BlockPos, state: BlockStateId) -> Self {
164 let base = Arc::new(BlockEntityBase::new(
165 &vanilla_block_entity_types::BEACON,
166 level,
167 pos,
168 state,
169 ));
170 Self {
171 base,
172 state: Arc::new(SyncMutex::new(BeaconState::new())),
173 }
174 }
175
176 pub(crate) fn state(&self) -> Arc<SyncMutex<BeaconState>> {
177 Arc::clone(&self.state)
178 }
179
180 pub(crate) fn base_handle(&self) -> Arc<BlockEntityBase> {
182 Arc::clone(&self.base)
183 }
184
185 pub(crate) fn play_sound(world: &World, pos: BlockPos, sound: SoundEventRef) {
186 world.play_block_sound(sound, pos, 1.0, 1.0, None);
187 }
188
189 fn advance_beam_scan(
193 state: &mut BeaconState,
194 world: &World,
195 pos: BlockPos,
196 last_set_block: i32,
197 ) {
198 let mut check_pos = if state.last_check_y < pos.y() {
199 state.checking_beam_sections.clear();
200 state.last_check_y = pos.y() - 1;
201 pos
202 } else {
203 BlockPos::new(pos.x(), state.last_check_y + 1, pos.z())
204 };
205
206 for _ in 0..BLOCKS_CHECK_PER_TICK {
207 if check_pos.y() > last_set_block {
208 break;
209 }
210
211 let block_state = world.get_block_state(check_pos);
212 let beam_color = BLOCK_BEHAVIORS
213 .get_behavior(block_state.get_block())
214 .beacon_beam_color(block_state);
215
216 if let Some(color) = beam_color {
217 let color = ArgbColor::new(color.texture_diffuse_color());
218 if state.checking_beam_sections.len() <= 1 {
219 state.checking_beam_sections.push(BeamSection::new(color));
220 } else if let Some(last) = state.checking_beam_sections.last_mut() {
221 if last.color == color {
222 last.increase_height();
223 } else {
224 let blended = last.color.average(color);
225 state.checking_beam_sections.push(BeamSection::new(blended));
226 }
227 }
228 } else {
229 let opaque = block_state.get_block() != &vanilla_blocks::BEDROCK
230 && block_state.get_light_dampening() >= MAX_LIGHT_LEVEL;
231 let Some(last) = state.checking_beam_sections.last_mut().filter(|_| !opaque) else {
232 state.checking_beam_sections.clear();
233 state.last_check_y = last_set_block;
234 return;
235 };
236 last.increase_height();
237 }
238
239 check_pos = check_pos.above();
240 state.last_check_y += 1;
241 }
242 }
243
244 fn update_base(world: &World, pos: BlockPos) -> i32 {
246 let mut levels = 0;
247 for step in 1..=MAX_LEVELS {
248 let layer_y = pos.y() - step;
249 if layer_y < world.get_min_y() {
250 break;
251 }
252
253 let mut valid = true;
254 'outer: for layer_x in (pos.x() - step)..=(pos.x() + step) {
255 for layer_z in (pos.z() - step)..=(pos.z() + step) {
256 let state = world.get_block_state(BlockPos::new(layer_x, layer_y, layer_z));
257 if !REGISTRY.blocks.is_in_tag(
258 state.get_block(),
259 &vanilla_block_tags::BlockTag::BEACON_BASE_BLOCKS,
260 ) {
261 valid = false;
262 break 'outer;
263 }
264 }
265 }
266
267 if !valid {
268 break;
269 }
270 levels = step;
271 }
272 levels
273 }
274
275 fn apply_effects(&self, world: &Arc<World>, pos: BlockPos, levels: i32) {
276 let (primary, secondary) = {
277 let state = self.state.lock();
278 (state.primary_power, state.secondary_power)
279 };
280 let Some(primary) = primary else {
281 return;
282 };
283
284 let range = f64::from(levels) * EFFECT_RANGE_PER_LEVEL + BASE_EFFECT_RANGE;
285 let base_amplifier =
286 i32::from(levels >= MAX_LEVELS && secondary.is_some_and(|s| s.key == primary.key));
287 let duration = (9 + levels * 2) * 20;
288
289 let world_height = f64::from(world.get_max_y() - world.get_min_y() + 1);
290 let min = DVec3::new(
291 f64::from(pos.x()) - range,
292 f64::from(pos.y()) - range,
293 f64::from(pos.z()) - range,
294 );
295 let max = DVec3::new(
296 f64::from(pos.x()) + 1.0 + range,
297 f64::from(pos.y()) + 1.0 + range + world_height,
298 f64::from(pos.z()) + 1.0 + range,
299 );
300 let aabb = WorldAabb::from_min_max(min, max);
301
302 for entity in world.get_entities_in_aabb_matching(&aabb, |entity| {
303 entity.entity_type() == &vanilla_entities::PLAYER && !entity.is_spectator()
304 }) {
305 let Some(player) = entity.downcast_ref::<Player>() else {
306 continue;
307 };
308 player.add_mob_effect(
309 MobEffectInstance::with_duration(primary, duration, base_amplifier)
310 .with_ambient(true)
311 .with_visible(true),
312 );
313
314 if let Some(secondary) =
315 secondary.filter(|s| levels >= MAX_LEVELS && s.key != primary.key)
316 {
317 player.add_mob_effect(
318 MobEffectInstance::with_duration(secondary, duration, 0)
319 .with_ambient(true)
320 .with_visible(true),
321 );
322 }
323 }
324 }
325
326 fn store_effect(nbt: &mut NbtCompound, field: &str, effect: Option<MobEffectRef>) {
327 if let Some(effect) = effect {
328 nbt.insert(field, effect.key.to_string());
329 }
330 }
331
332 fn load_effect(nbt: &BorrowedNbtCompound<'_>, field: &str) -> Option<MobEffectRef> {
333 let nbt_view: NbtCompoundView<'_, '_> = nbt.into();
334 let key = Identifier::from_str(&nbt_view.string(field)?.to_string()).ok()?;
335 let effect = REGISTRY.mob_effects.by_key(&key)?;
336 BeaconState::filter_effect(Some(effect))
337 }
338}
339
340impl BlockEntity for BeaconBlockEntity {
341 fn base(&self) -> &BlockEntityBase {
342 &self.base
343 }
344
345 fn load_additional(&self, nbt: &BorrowedNbtCompound<'_>) {
348 let mut state = self.state.lock();
349 state.primary_power = Self::load_effect(nbt, "primary_effect");
350 state.secondary_power = Self::load_effect(nbt, "secondary_effect");
351 }
352
353 fn save_additional(&self, nbt: &mut NbtCompound) {
354 let state = self.state.lock();
355 Self::store_effect(nbt, "primary_effect", state.primary_power);
356 Self::store_effect(nbt, "secondary_effect", state.secondary_power);
357 nbt.insert("Levels", state.levels);
358 }
359
360 fn get_update_tag(&self) -> Option<NbtCompound> {
361 let mut nbt = NbtCompound::new();
362 {
363 let state = self.state.lock();
364 Self::store_effect(&mut nbt, "primary_effect", state.primary_power);
365 Self::store_effect(&mut nbt, "secondary_effect", state.secondary_power);
366 nbt.insert("Levels", state.levels);
367 }
368 Some(nbt)
369 }
370
371 fn tick(&self, world: &Arc<World>) {
372 let pos = self.get_block_pos();
373 let last_set_block = world.level_height_at(HeightmapType::WorldSurface, pos.x(), pos.z());
374 let is_interval_tick = world.game_time() % BEACON_TICK_INTERVAL == 0;
375
376 let (previous_levels, levels, had_beam, scan_complete) = {
377 let mut state = self.state.lock();
378 Self::advance_beam_scan(&mut state, world, pos, last_set_block);
379
380 let previous_levels = state.levels;
381 let had_beam = !state.beam_sections.is_empty();
383 if is_interval_tick && had_beam {
384 state.levels = Self::update_base(world, pos);
385 }
386
387 let scan_complete = state.last_check_y >= last_set_block;
388 if scan_complete {
389 state.last_check_y = world.get_min_y() - 1;
390 state.beam_sections = mem::take(&mut state.checking_beam_sections);
391 }
392
393 (previous_levels, state.levels, had_beam, scan_complete)
394 };
395
396 if is_interval_tick && levels > 0 && had_beam {
397 self.apply_effects(world, pos, levels);
398 Self::play_sound(world, pos, &sound_events::BLOCK_BEACON_AMBIENT);
399 }
400
401 if scan_complete {
402 let sound = match (previous_levels > 0, levels > 0) {
405 (false, true) => &sound_events::BLOCK_BEACON_ACTIVATE,
406 (true, false) => &sound_events::BLOCK_BEACON_DEACTIVATE,
407 _ => return,
408 };
409 Self::play_sound(world, pos, sound);
410 }
411 }
412
413 fn on_set_removed(&self) {
414 let Some(world) = self.get_level() else {
415 return;
416 };
417 Self::play_sound(
418 &world,
419 self.get_block_pos(),
420 &sound_events::BLOCK_BEACON_DEACTIVATE,
421 );
422 }
423}