steel_core/block_entity/entities/
conduit.rs1use std::sync::{Arc, Weak};
7
8use glam::DVec3;
9use simdnbt::borrow::{BaseNbtCompound as BorrowedNbtCompound, NbtCompound as NbtCompoundView};
10use simdnbt::owned::NbtCompound;
11use steel_protocol::packets::game::SoundSource;
12use steel_registry::blocks::block_state_ext::BlockStateExt as _;
13use steel_registry::{
14 REGISTRY, TaggedRegistryExt, sound_events, vanilla_block_entity_types, vanilla_block_tags,
15 vanilla_damage_types, vanilla_entities, vanilla_mob_effects,
16};
17use steel_utils::locks::SyncMutex;
18use steel_utils::{
19 BlockLocalAabb, BlockPos, BlockStateId, Downcast as _, DowncastType, DowncastTypeKey,
20};
21
22use crate::block_entity::{BlockEntity, BlockEntityBase};
23use crate::entity::damage::DamageSource;
24use crate::entity::{
25 Entity as _, EntityReference, LivingEntity as _, MobEffectInstance, SharedEntity,
26};
27use crate::fluid::{FluidStateExt as _, get_fluid_state};
28use crate::player::Player;
29use crate::world::World;
30
31const BLOCK_REFRESH_RATE: i64 = 40;
32const AMBIENT_SOUND_INTERVAL: i64 = 80;
33const EFFECT_DURATION: i32 = 13 * 20;
34const MIN_ACTIVE_SIZE: usize = 16;
35const MIN_KILL_SIZE: usize = 42;
36const KILL_RANGE: f64 = 8.0;
37const ATTACK_DAMAGE: f32 = 4.0;
38
39const FRAME_OFFSETS: [(i32, i32, i32); MIN_KILL_SIZE] = {
42 let mut offsets = [(0, 0, 0); MIN_KILL_SIZE];
43 let mut count = 0;
44 let mut ox: i32 = -2;
45 while ox <= 2 {
46 let mut oy: i32 = -2;
47 while oy <= 2 {
48 let mut oz: i32 = -2;
49 while oz <= 2 {
50 let (ax, ay, az) = (ox.abs(), oy.abs(), oz.abs());
51 let outside_cage = ax > 1 || ay > 1 || az > 1;
52 let on_ring = ox == 0 && (ay == 2 || az == 2)
53 || oy == 0 && (ax == 2 || az == 2)
54 || oz == 0 && (ax == 2 || ay == 2);
55 if outside_cage && on_ring {
56 offsets[count] = (ox, oy, oz);
57 count += 1;
58 }
59 oz += 1;
60 }
61 oy += 1;
62 }
63 ox += 1;
64 }
65 assert!(
66 count == MIN_KILL_SIZE,
67 "a full conduit frame is the hunting size"
68 );
69 offsets
70};
71
72struct ConduitState {
73 is_active: bool,
74 is_hunting: bool,
75 destroy_target: Option<EntityReference>,
76 next_ambient_sound_activation: i64,
77}
78
79pub struct ConduitBlockEntity {
81 base: BlockEntityBase,
82 state: SyncMutex<ConduitState>,
83}
84
85unsafe impl DowncastType for ConduitBlockEntity {
87 const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:block_entity/conduit");
88}
89
90impl ConduitBlockEntity {
91 #[must_use]
93 pub fn new(level: Weak<World>, pos: BlockPos, state: BlockStateId) -> Self {
94 Self {
95 base: BlockEntityBase::new(&vanilla_block_entity_types::CONDUIT, level, pos, state),
96 state: SyncMutex::new(ConduitState {
97 is_active: false,
98 is_hunting: false,
99 destroy_target: None,
100 next_ambient_sound_activation: 0,
101 }),
102 }
103 }
104
105 #[must_use]
107 pub fn is_active(&self) -> bool {
108 self.state.lock().is_active
109 }
110
111 #[must_use]
113 pub fn is_hunting(&self) -> bool {
114 self.state.lock().is_hunting
115 }
116
117 fn update_shape(world: &Arc<World>, pos: BlockPos) -> usize {
119 for ox in -1..=1 {
120 for oy in -1..=1 {
121 for oz in -1..=1 {
122 if !get_fluid_state(world, pos.offset(ox, oy, oz)).is_water() {
123 return 0;
124 }
125 }
126 }
127 }
128
129 FRAME_OFFSETS
130 .iter()
131 .filter(|&&(ox, oy, oz)| {
132 let state = world.get_block_state(pos.offset(ox, oy, oz));
133 REGISTRY.blocks.is_in_tag(
134 state.get_block(),
135 &vanilla_block_tags::BlockTag::CONDUIT_EFFECT_BLOCK,
136 )
137 })
138 .count()
139 }
140
141 fn apply_effects(world: &Arc<World>, pos: BlockPos, active_size: usize) {
142 let effect_range = f64::from((active_size / 7 * 16) as i32);
143 let aabb = BlockLocalAabb::FULL_BLOCK
144 .at_block(pos)
145 .inflate(effect_range)
146 .expand_towards(DVec3::new(0.0, f64::from(world.get_height()), 0.0));
147
148 for entity in world.get_entities_in_aabb_matching(&aabb, |entity| {
149 entity.entity_type() == &vanilla_entities::PLAYER && !entity.is_spectator()
150 }) {
151 let Some(player) = entity.downcast_ref::<Player>() else {
152 continue;
153 };
154 if !pos.closer_than(player.block_position(), effect_range)
155 || !player.is_in_water_or_rain()
156 {
157 continue;
158 }
159 player.add_mob_effect(
160 MobEffectInstance::with_duration(
161 vanilla_mob_effects::CONDUIT_POWER,
162 EFFECT_DURATION,
163 0,
164 )
165 .with_ambient(true)
166 .with_visible(true),
167 );
168 }
169 }
170
171 fn update_and_attack_target(&self, world: &Arc<World>, pos: BlockPos, is_hunting: bool) {
172 let current_target = self.state.lock().destroy_target.clone();
173 let new_target =
174 Self::update_destroy_target(current_target.as_ref(), world, pos, is_hunting);
175
176 if let Some(target) = new_target
177 .as_ref()
178 .and_then(|t| Self::living_entity(t, world))
179 {
180 world.play_sound_at(
181 &sound_events::BLOCK_CONDUIT_ATTACK_TARGET,
182 SoundSource::Blocks,
183 target.position(),
184 1.0,
185 1.0,
186 None,
187 );
188 if let Some(living) = target.as_living_entity() {
189 living.hurt_server(
190 world,
191 &DamageSource::environment(&vanilla_damage_types::MAGIC),
192 ATTACK_DAMAGE,
193 );
194 }
195 }
196
197 let changed = {
198 let mut state = self.state.lock();
199 let changed = state.destroy_target.as_ref().map(EntityReference::uuid)
200 != new_target.as_ref().map(EntityReference::uuid);
201 if changed {
202 state.destroy_target = new_target;
203 }
204 changed
205 };
206 if changed {
207 world.send_block_updated(pos);
208 }
209 }
210
211 fn update_destroy_target(
212 target: Option<&EntityReference>,
213 world: &Arc<World>,
214 pos: BlockPos,
215 is_hunting: bool,
216 ) -> Option<EntityReference> {
217 if !is_hunting {
218 return None;
219 }
220
221 let Some(target) = target else {
222 return Self::select_new_target(world, pos);
223 };
224
225 let entity = Self::living_entity(target, world)?;
226 (entity.is_alive() && pos.closer_than(entity.block_position(), KILL_RANGE))
227 .then(|| target.clone())
228 }
229
230 fn select_new_target(world: &Arc<World>, pos: BlockPos) -> Option<EntityReference> {
231 let aabb = BlockLocalAabb::FULL_BLOCK.at_block(pos).inflate(KILL_RANGE);
232 let candidates = world.get_entities_in_aabb_matching(&aabb, |entity| {
233 entity.as_living_entity().is_some() && entity.is_enemy() && entity.is_in_water_or_rain()
234 });
235 if candidates.is_empty() {
236 return None;
237 }
238 let chosen = &candidates[rand::random_range(0..candidates.len())];
239 Some(EntityReference::from_entity(chosen))
240 }
241
242 fn living_entity(reference: &EntityReference, world: &World) -> Option<SharedEntity> {
243 reference
244 .get_entity(world)
245 .filter(|entity| entity.as_living_entity().is_some())
246 }
247}
248
249impl BlockEntity for ConduitBlockEntity {
250 fn base(&self) -> &BlockEntityBase {
251 &self.base
252 }
253
254 fn load_additional(&self, nbt: &BorrowedNbtCompound<'_>) {
255 let nbt_view: NbtCompoundView<'_, '_> = nbt.into();
256 self.state.lock().destroy_target = EntityReference::read(&nbt_view, "Target");
257 }
258
259 fn save_additional(&self, nbt: &mut NbtCompound) {
260 if let Some(target) = &self.state.lock().destroy_target {
261 target.store(nbt, "Target");
262 }
263 }
264
265 fn get_update_tag(&self) -> Option<NbtCompound> {
266 Some(self.save_custom_only())
267 }
268
269 fn tick(&self, world: &Arc<World>) {
270 let pos = self.get_block_pos();
271 let game_time = world.game_time();
272
273 if game_time % BLOCK_REFRESH_RATE == 0 {
274 let effect_blocks = Self::update_shape(world, pos);
275 let active = effect_blocks >= MIN_ACTIVE_SIZE;
276 let hunting = effect_blocks >= MIN_KILL_SIZE;
277
278 let was_active = {
279 let mut state = self.state.lock();
280 let was_active = state.is_active;
281 state.is_active = active;
282 state.is_hunting = hunting;
283 was_active
284 };
285 if active != was_active {
286 let sound = if active {
287 &sound_events::BLOCK_CONDUIT_ACTIVATE
288 } else {
289 &sound_events::BLOCK_CONDUIT_DEACTIVATE
290 };
291 world.play_block_sound(sound, pos, 1.0, 1.0, None);
292 }
293
294 if active {
295 Self::apply_effects(world, pos, effect_blocks);
296 self.update_and_attack_target(world, pos, hunting);
297 }
298 }
299
300 let play_short_ambient = {
301 let mut state = self.state.lock();
302 if !state.is_active {
303 return;
304 }
305 let play = game_time > state.next_ambient_sound_activation;
306 if play {
307 state.next_ambient_sound_activation = game_time + 60 + rand::random_range(0..40);
308 }
309 play
310 };
311
312 if game_time % AMBIENT_SOUND_INTERVAL == 0 {
313 world.play_block_sound(&sound_events::BLOCK_CONDUIT_AMBIENT, pos, 1.0, 1.0, None);
314 }
315 if play_short_ambient {
316 world.play_block_sound(
317 &sound_events::BLOCK_CONDUIT_AMBIENT_SHORT,
318 pos,
319 1.0,
320 1.0,
321 None,
322 );
323 }
324 }
325}