1#![expect(
2 clippy::too_many_arguments,
3 reason = "sculk spreading helpers mirror vanilla cursor state"
4)]
5
6use super::super::prelude::*;
7use super::super::runner::FeatureDecorationRunner;
8use core::mem;
9use steel_registry::vanilla_block_entity_types;
10use steel_registry::vanilla_block_tags::BlockTag;
11
12const SCULK_DEFAULT_SPREAD_TYPES: [SculkSpreadType; 3] = [
13 SculkSpreadType::SamePosition,
14 SculkSpreadType::SamePlane,
15 SculkSpreadType::WrapAround,
16];
17const SCULK_SAME_SPACE_SPREAD_TYPES: [SculkSpreadType; 1] = [SculkSpreadType::SamePosition];
18
19#[derive(Clone, Copy)]
20enum SculkSpreadType {
21 SamePosition,
22 SamePlane,
23 WrapAround,
24}
25
26struct SculkSpreadPos {
27 pos: BlockPos,
28 face: Direction,
29}
30
31#[derive(Clone, Copy)]
32enum SculkBehaviorKind {
33 Default,
34 Sculk,
35 SculkVein,
36}
37
38struct SculkSpreader {
39 is_world_generation: bool,
40 replaceable_blocks: Identifier,
41 growth_spawn_cost: i32,
42 no_growth_radius: i32,
43 charge_decay_rate: i32,
44 additional_decay_rate: i32,
45 cursors: Vec<SculkChargeCursor>,
46}
47
48impl SculkSpreader {
49 const MAX_CHARGE: i32 = 1000;
50 const MAX_CURSORS: usize = 32;
51
52 const fn worldgen() -> Self {
53 Self {
54 is_world_generation: true,
55 replaceable_blocks: BlockTag::SCULK_REPLACEABLE_WORLD_GEN,
56 growth_spawn_cost: 50,
57 no_growth_radius: 1,
58 charge_decay_rate: 5,
59 additional_decay_rate: 10,
60 cursors: Vec::new(),
61 }
62 }
63
64 fn add_cursors(&mut self, start_pos: BlockPos, mut charge: i32) {
65 while charge > 0 {
66 let current_charge = charge.min(Self::MAX_CHARGE);
67 self.add_cursor(SculkChargeCursor::new(start_pos, current_charge));
68 charge -= current_charge;
69 }
70 }
71
72 fn add_cursor(&mut self, cursor: SculkChargeCursor) {
73 if self.cursors.len() < Self::MAX_CURSORS {
74 self.cursors.push(cursor);
75 }
76 }
77
78 fn clear(&mut self) {
79 self.cursors.clear();
80 }
81}
82
83struct SculkChargeCursor {
84 pos: BlockPos,
85 charge: i32,
86 update_delay: i32,
87 decay_delay: i32,
88 facings: Option<Vec<Direction>>,
89}
90
91impl SculkChargeCursor {
92 const MAX_CURSOR_DISTANCE: i32 = 1024;
93
94 const fn new(pos: BlockPos, charge: i32) -> Self {
95 Self {
96 pos,
97 charge,
98 update_delay: 0,
99 decay_delay: 1,
100 facings: None,
101 }
102 }
103}
104
105impl FeatureDecorationRunner {
106 pub(in crate::worldgen::feature) fn place_sculk_patch_feature(
107 region: &mut WorldGenRegion<'_>,
108 registry: &Registry,
109 random: &mut WorldgenRandom,
110 config: &SculkPatchConfiguration,
111 origin: BlockPos,
112 ) -> bool {
113 if !Self::can_sculk_spread_from(region, origin) {
114 return false;
115 }
116
117 let mut spreader = SculkSpreader::worldgen();
118 let total_rounds = config.spread_rounds + config.growth_rounds;
119 for round in 0..total_rounds {
120 for _ in 0..config.charge_count {
121 spreader.add_cursors(origin, config.amount_per_charge);
122 }
123
124 let spread_veins = round < config.spread_rounds;
125 for _ in 0..config.spread_attempts {
126 Self::sculk_update_cursors(
127 region,
128 registry,
129 random,
130 origin,
131 &mut spreader,
132 spread_veins,
133 );
134 }
135
136 spreader.clear();
137 }
138
139 let below = origin.below();
140 let below_state = region.block_state(below);
141 if random.next_f32() <= config.catalyst_chance
142 && shapes::is_offset_shape_full_block(below_state.get_collision_shape_at(below))
143 {
144 let catalyst = vanilla_blocks::SCULK_CATALYST.default_state();
145 if region.set_block_state(origin, catalyst, UpdateFlags::UPDATE_ALL) {
146 Self::set_empty_block_entity(
147 region,
148 origin,
149 &vanilla_block_entity_types::SCULK_CATALYST,
150 catalyst,
151 );
152 }
153 }
154
155 let extra_growths = config.extra_rare_growths.sample(random);
156 for _ in 0..extra_growths {
157 let candidate = origin.offset(
158 random.next_i32_bounded(5) - 2,
159 0,
160 random.next_i32_bounded(5) - 2,
161 );
162 let below = candidate.below();
163 if !region.block_state(candidate).is_air()
164 || !region
165 .block_state(below)
166 .is_face_sturdy_at(below, Direction::Up)
167 {
168 continue;
169 }
170
171 let shrieker = vanilla_blocks::SCULK_SHRIEKER
172 .default_state()
173 .set_value(&BlockStateProperties::CAN_SUMMON, true);
174 if region.set_block_state(candidate, shrieker, UpdateFlags::UPDATE_ALL) {
175 Self::set_empty_block_entity(
176 region,
177 candidate,
178 &vanilla_block_entity_types::SCULK_SHRIEKER,
179 shrieker,
180 );
181 }
182 }
183
184 true
185 }
186
187 fn can_sculk_spread_from(region: &WorldGenRegion<'_>, origin: BlockPos) -> bool {
188 let start = region.block_state(origin);
189 if !matches!(Self::sculk_behavior(start), SculkBehaviorKind::Default) {
190 return true;
191 }
192
193 if !start.is_air()
194 && (start.get_block() != &vanilla_blocks::WATER
195 || !get_fluid_state_from_block(start).is_source())
196 {
197 return false;
198 }
199
200 Self::VANILLA_DIRECTION_VALUES.iter().any(|direction| {
201 let pos = origin.relative(*direction);
202 let state = region.block_state(pos);
203 shapes::is_offset_shape_full_block(state.get_collision_shape_at(pos))
204 })
205 }
206
207 fn sculk_update_cursors(
208 region: &mut WorldGenRegion<'_>,
209 registry: &Registry,
210 random: &mut WorldgenRandom,
211 origin: BlockPos,
212 spreader: &mut SculkSpreader,
213 spread_veins: bool,
214 ) {
215 if spreader.cursors.is_empty() {
216 return;
217 }
218
219 let cursors = mem::take(&mut spreader.cursors);
220 for mut cursor in cursors {
221 if Self::sculk_cursor_is_pos_unreasonable(cursor.pos, origin) {
222 continue;
223 }
224
225 Self::sculk_update_cursor(
226 region,
227 registry,
228 random,
229 origin,
230 spreader,
231 spread_veins,
232 &mut cursor,
233 );
234 if cursor.charge > 0 {
235 spreader.cursors.push(cursor);
236 }
237 }
238 }
239
240 fn sculk_update_cursor(
241 region: &mut WorldGenRegion<'_>,
242 registry: &Registry,
243 random: &mut WorldgenRandom,
244 origin: BlockPos,
245 spreader: &SculkSpreader,
246 spread_veins: bool,
247 cursor: &mut SculkChargeCursor,
248 ) {
249 if cursor.charge <= 0 {
250 return;
251 }
252
253 if cursor.update_delay > 0 {
254 cursor.update_delay -= 1;
255 return;
256 }
257
258 let mut current_state = region.block_state(cursor.pos);
259 let mut behavior = Self::sculk_behavior(current_state);
260 if spread_veins
261 && Self::sculk_attempt_spread_vein(
262 region,
263 cursor.pos,
264 current_state,
265 cursor.facings.as_deref(),
266 spreader.is_world_generation,
267 behavior,
268 )
269 && Self::sculk_can_change_block_state_on_spread(behavior)
270 {
271 current_state = region.block_state(cursor.pos);
272 behavior = Self::sculk_behavior(current_state);
273 }
274
275 cursor.charge = Self::sculk_attempt_use_charge(
276 region,
277 registry,
278 random,
279 origin,
280 spreader,
281 spread_veins,
282 cursor,
283 behavior,
284 );
285 if cursor.charge <= 0 {
286 Self::sculk_on_discharged(region, current_state, cursor.pos);
287 return;
288 }
289
290 let transfer_pos = Self::sculk_get_valid_movement_pos(region, cursor.pos, random);
291 if let Some(transfer_pos) = transfer_pos {
292 Self::sculk_on_discharged(region, current_state, cursor.pos);
293 cursor.pos = transfer_pos;
294 if spreader.is_world_generation
295 && !Self::sculk_horizontal_close_to_origin(cursor.pos, origin, 15.0)
296 {
297 cursor.charge = 0;
298 return;
299 }
300 current_state = region.block_state(transfer_pos);
301 }
302
303 if !matches!(
304 Self::sculk_behavior(current_state),
305 SculkBehaviorKind::Default
306 ) {
307 cursor.facings = Some(Self::sculk_available_faces(current_state));
308 }
309
310 cursor.decay_delay = Self::sculk_update_decay_delay(behavior, cursor.decay_delay);
311 cursor.update_delay = Self::sculk_spread_delay(behavior);
312 }
313
314 fn sculk_attempt_spread_vein(
315 region: &mut WorldGenRegion<'_>,
316
317 pos: BlockPos,
318 state: BlockStateId,
319 facings: Option<&[Direction]>,
320 post_process: bool,
321 behavior: SculkBehaviorKind,
322 ) -> bool {
323 match behavior {
324 SculkBehaviorKind::Default => match facings {
325 None => Self::sculk_vein_spread_all(region, state, pos, post_process, true) > 0,
326 Some(faces) if !faces.is_empty() => {
327 if !state.is_air() && !get_fluid_state_from_block(state).is_water() {
328 return false;
329 }
330 Self::sculk_vein_regrow(region, pos, state, faces)
331 }
332 Some(_) => Self::sculk_vein_spread_all(region, state, pos, post_process, false) > 0,
333 },
334 SculkBehaviorKind::Sculk | SculkBehaviorKind::SculkVein => {
335 Self::sculk_vein_spread_all(region, state, pos, post_process, false) > 0
336 }
337 }
338 }
339
340 fn sculk_attempt_use_charge(
341 region: &mut WorldGenRegion<'_>,
342 registry: &Registry,
343 random: &mut WorldgenRandom,
344 origin: BlockPos,
345 spreader: &SculkSpreader,
346 spread_veins: bool,
347 cursor: &SculkChargeCursor,
348 behavior: SculkBehaviorKind,
349 ) -> i32 {
350 match behavior {
351 SculkBehaviorKind::Default => {
352 if cursor.decay_delay > 0 {
353 cursor.charge
354 } else {
355 0
356 }
357 }
358 SculkBehaviorKind::Sculk => {
359 Self::sculk_block_attempt_use_charge(region, random, origin, spreader, cursor)
360 }
361 SculkBehaviorKind::SculkVein => Self::sculk_vein_attempt_use_charge(
362 region,
363 registry,
364 random,
365 spreader,
366 spread_veins,
367 cursor,
368 ),
369 }
370 }
371
372 fn sculk_block_attempt_use_charge(
373 region: &mut WorldGenRegion<'_>,
374 random: &mut WorldgenRandom,
375 origin: BlockPos,
376 spreader: &SculkSpreader,
377 cursor: &SculkChargeCursor,
378 ) -> i32 {
379 let charge = cursor.charge;
380 if charge == 0 || random.next_i32_bounded(spreader.charge_decay_rate) != 0 {
381 return charge;
382 }
383
384 let is_close_to_catalyst =
385 Self::sculk_closer_than(cursor.pos, origin, spreader.no_growth_radius);
386 if !is_close_to_catalyst && Self::sculk_can_place_growth(region, cursor.pos) {
387 if random.next_i32_bounded(spreader.growth_spawn_cost) < charge {
388 let growth_pos = cursor.pos.above();
389 let growth_state = Self::sculk_random_growth_state(
390 region,
391 random,
392 growth_pos,
393 spreader.is_world_generation,
394 );
395 if region.set_block_state(growth_pos, growth_state, UpdateFlags::UPDATE_ALL) {
396 Self::set_sculk_growth_block_entity(region, growth_pos, growth_state);
397 }
398 }
399
400 0.max(charge - spreader.growth_spawn_cost)
401 } else if random.next_i32_bounded(spreader.additional_decay_rate) != 0 {
402 charge
403 } else if is_close_to_catalyst {
404 charge - 1
405 } else {
406 charge - Self::sculk_decay_penalty(spreader, cursor.pos, origin, charge)
407 }
408 }
409
410 fn sculk_vein_attempt_use_charge(
411 region: &mut WorldGenRegion<'_>,
412 registry: &Registry,
413 random: &mut WorldgenRandom,
414 spreader: &SculkSpreader,
415 spread_veins: bool,
416 cursor: &SculkChargeCursor,
417 ) -> i32 {
418 if spread_veins
419 && Self::sculk_vein_attempt_place_sculk(region, registry, random, spreader, cursor.pos)
420 {
421 cursor.charge - 1
422 } else if random.next_i32_bounded(spreader.charge_decay_rate) == 0 {
423 fast_floor(f64::from(cursor.charge) * 0.5) as i32
424 } else {
425 cursor.charge
426 }
427 }
428
429 fn sculk_vein_attempt_place_sculk(
430 region: &mut WorldGenRegion<'_>,
431 registry: &Registry,
432 random: &mut WorldgenRandom,
433 spreader: &SculkSpreader,
434 pos: BlockPos,
435 ) -> bool {
436 let state = region.block_state(pos);
437 let directions = Self::shuffled_directions(random, Self::VANILLA_DIRECTION_VALUES);
438 for support in directions {
439 if !Self::sculk_vein_has_face(state, support) {
440 continue;
441 }
442
443 let support_pos = pos.relative(support);
444 let support_state = region.block_state(support_pos);
445 if !registry
446 .blocks
447 .is_in_tag(support_state.get_block(), &spreader.replaceable_blocks)
448 {
449 continue;
450 }
451
452 let sculk = vanilla_blocks::SCULK.default_state();
453 let _ = region.set_block_state(support_pos, sculk, UpdateFlags::UPDATE_ALL);
454 let _ = Self::sculk_vein_spread_all(
455 region,
456 sculk,
457 support_pos,
458 spreader.is_world_generation,
459 false,
460 );
461
462 let skip = support.opposite();
463 for direction in Self::VANILLA_DIRECTION_VALUES {
464 if direction == skip {
465 continue;
466 }
467
468 let vein_pos = support_pos.relative(direction);
469 let possible_vein = region.block_state(vein_pos);
470 if possible_vein.get_block() == &vanilla_blocks::SCULK_VEIN {
471 Self::sculk_on_discharged(region, possible_vein, vein_pos);
472 }
473 }
474
475 return true;
476 }
477
478 false
479 }
480
481 fn sculk_vein_spread_all(
482 region: &mut WorldGenRegion<'_>,
483
484 state: BlockStateId,
485 pos: BlockPos,
486 post_process: bool,
487 same_space_only: bool,
488 ) -> i64 {
489 let mut count = 0;
490 for starting_face in Self::VANILLA_DIRECTION_VALUES {
491 if !Self::sculk_vein_can_spread_from(state, starting_face) {
492 continue;
493 }
494
495 for spread_direction in Self::VANILLA_DIRECTION_VALUES {
496 if Self::sculk_vein_spread_from_face_toward_direction(
497 region,
498 state,
499 pos,
500 starting_face,
501 spread_direction,
502 post_process,
503 same_space_only,
504 )
505 .is_some()
506 {
507 count += 1;
508 }
509 }
510 }
511 count
512 }
513
514 fn sculk_vein_spread_from_face_toward_direction(
515 region: &mut WorldGenRegion<'_>,
516
517 state: BlockStateId,
518 pos: BlockPos,
519 starting_face: Direction,
520 spread_direction: Direction,
521 post_process: bool,
522 same_space_only: bool,
523 ) -> Option<SculkSpreadPos> {
524 let spread_pos = Self::sculk_vein_get_spread_from_face_toward_direction(
525 region,
526 state,
527 pos,
528 starting_face,
529 spread_direction,
530 same_space_only,
531 )?;
532 if Self::sculk_vein_spread_to_face(region, &spread_pos, post_process) {
533 Some(spread_pos)
534 } else {
535 None
536 }
537 }
538
539 fn sculk_vein_get_spread_from_face_toward_direction(
540 region: &WorldGenRegion<'_>,
541
542 state: BlockStateId,
543 pos: BlockPos,
544 starting_face: Direction,
545 spread_direction: Direction,
546 same_space_only: bool,
547 ) -> Option<SculkSpreadPos> {
548 if spread_direction.axis() == starting_face.axis() {
549 return None;
550 }
551
552 if !Self::sculk_vein_is_other_block_valid_as_source(state)
553 && (!Self::sculk_vein_has_face(state, starting_face)
554 || Self::sculk_vein_has_face(state, spread_direction))
555 {
556 return None;
557 }
558
559 let spread_types = if same_space_only {
560 SCULK_SAME_SPACE_SPREAD_TYPES.as_slice()
561 } else {
562 SCULK_DEFAULT_SPREAD_TYPES.as_slice()
563 };
564 for spread_type in spread_types {
565 let spread_pos =
566 Self::sculk_vein_spread_pos(pos, spread_direction, starting_face, *spread_type);
567 if Self::sculk_vein_can_spread_into(region, pos, &spread_pos) {
568 return Some(spread_pos);
569 }
570 }
571
572 None
573 }
574
575 fn sculk_vein_spread_to_face(
576 region: &mut WorldGenRegion<'_>,
577
578 spread_pos: &SculkSpreadPos,
579 post_process: bool,
580 ) -> bool {
581 let old_state = region.block_state(spread_pos.pos);
582 let Some(spread_state) = Self::sculk_vein_state_for_placement(
583 region,
584 old_state,
585 spread_pos.pos,
586 spread_pos.face,
587 ) else {
588 return false;
589 };
590
591 if post_process {
592 region.mark_pos_for_postprocessing(spread_pos.pos);
593 }
594 region.set_block_state(spread_pos.pos, spread_state, UpdateFlags::UPDATE_CLIENTS)
595 }
596
597 fn sculk_vein_can_spread_into(
598 region: &WorldGenRegion<'_>,
599 source_pos: BlockPos,
600 spread_pos: &SculkSpreadPos,
601 ) -> bool {
602 let existing_state = region.block_state(spread_pos.pos);
603 Self::sculk_patch_vein_state_can_be_replaced(
604 region,
605 source_pos,
606 spread_pos.pos,
607 spread_pos.face,
608 existing_state,
609 ) && Self::sculk_vein_is_valid_state_for_placement(
610 region,
611 existing_state,
612 spread_pos.pos,
613 spread_pos.face,
614 )
615 }
616
617 fn sculk_patch_vein_state_can_be_replaced(
618 region: &WorldGenRegion<'_>,
619 source_pos: BlockPos,
620 placement_pos: BlockPos,
621 placement_direction: Direction,
622 existing_state: BlockStateId,
623 ) -> bool {
624 let against_state = region.block_state(placement_pos.relative(placement_direction));
625 if against_state.get_block() == &vanilla_blocks::SCULK
626 || against_state.get_block() == &vanilla_blocks::SCULK_CATALYST
627 || against_state.get_block() == &vanilla_blocks::MOVING_PISTON
628 {
629 return false;
630 }
631
632 if Self::manhattan_distance(source_pos, placement_pos) == 2 {
633 let neighbor_pos = source_pos.relative(placement_direction.opposite());
634 if region
635 .block_state(neighbor_pos)
636 .is_face_sturdy_at(neighbor_pos, placement_direction)
637 {
638 return false;
639 }
640 }
641
642 let fluid_state = get_fluid_state_from_block(existing_state);
643 if !fluid_state.is_empty() && !fluid_state.is_water() {
644 return false;
645 }
646
647 if existing_state.get_block().has_tag(&BlockTag::FIRE) {
648 return false;
649 }
650
651 existing_state.is_replaceable()
652 || Self::sculk_default_multiface_state_can_be_replaced_for_block(existing_state)
653 }
654
655 fn sculk_default_multiface_state_can_be_replaced_for_block(
656 existing_state: BlockStateId,
657 ) -> bool {
658 existing_state.is_air()
659 || existing_state.get_block() == &vanilla_blocks::SCULK_VEIN
660 || (existing_state.get_block() == &vanilla_blocks::WATER
661 && get_fluid_state_from_block(existing_state).is_source())
662 }
663
664 fn sculk_vein_state_for_placement(
665 region: &WorldGenRegion<'_>,
666 old_state: BlockStateId,
667 placement_pos: BlockPos,
668 placement_direction: Direction,
669 ) -> Option<BlockStateId> {
670 if !Self::sculk_vein_is_valid_state_for_placement(
671 region,
672 old_state,
673 placement_pos,
674 placement_direction,
675 ) {
676 return None;
677 }
678
679 let mut new_state = if old_state.get_block() == &vanilla_blocks::SCULK_VEIN {
680 old_state
681 } else {
682 let state = vanilla_blocks::SCULK_VEIN.default_state();
683 let fluid_state = get_fluid_state_from_block(old_state);
684 if fluid_state.is_water() && fluid_state.is_source() {
685 state.set_value(&BlockStateProperties::WATERLOGGED, true)
686 } else {
687 state
688 }
689 };
690 new_state = new_state.set_value(Self::sculk_vein_face_property(placement_direction), true);
691 Some(new_state)
692 }
693
694 fn sculk_vein_is_valid_state_for_placement(
695 region: &WorldGenRegion<'_>,
696 old_state: BlockStateId,
697 placement_pos: BlockPos,
698 placement_direction: Direction,
699 ) -> bool {
700 if old_state.get_block() == &vanilla_blocks::SCULK_VEIN
701 && Self::sculk_vein_has_face(old_state, placement_direction)
702 {
703 return false;
704 }
705
706 Self::can_attach_to_multiface(region, placement_pos, placement_direction)
707 }
708
709 fn sculk_vein_regrow(
710 region: &mut WorldGenRegion<'_>,
711 pos: BlockPos,
712 existing_state: BlockStateId,
713 faces: &[Direction],
714 ) -> bool {
715 let mut has_face = false;
716 let mut new_state = vanilla_blocks::SCULK_VEIN.default_state();
717
718 for &face in faces {
719 if Self::can_attach_to_multiface(region, pos, face) {
720 new_state = new_state.set_value(Self::sculk_vein_face_property(face), true);
721 has_face = true;
722 }
723 }
724
725 if !has_face {
726 return false;
727 }
728
729 if !get_fluid_state_from_block(existing_state).is_empty() {
730 new_state = new_state.set_value(&BlockStateProperties::WATERLOGGED, true);
731 }
732
733 region.set_block_state(pos, new_state, UpdateFlags::UPDATE_ALL)
734 }
735
736 fn sculk_on_discharged(
737 region: &mut WorldGenRegion<'_>,
738 mut state: BlockStateId,
739 pos: BlockPos,
740 ) {
741 if state.get_block() != &vanilla_blocks::SCULK_VEIN {
742 return;
743 }
744
745 for direction in Self::VANILLA_DIRECTION_VALUES {
746 if Self::sculk_vein_has_face(state, direction)
747 && region.block_state(pos.relative(direction)).get_block() == &vanilla_blocks::SCULK
748 {
749 state = state.set_value(Self::sculk_vein_face_property(direction), false);
750 }
751 }
752
753 if !Self::sculk_vein_has_any_face(state) {
754 state = if get_fluid_state_from_block(state).is_empty() {
755 vanilla_blocks::AIR.default_state()
756 } else {
757 vanilla_blocks::WATER.default_state()
758 };
759 }
760
761 let _ = region.set_block_state(pos, state, UpdateFlags::UPDATE_ALL);
762 }
763
764 fn sculk_get_valid_movement_pos(
765 region: &WorldGenRegion<'_>,
766 pos: BlockPos,
767 random: &mut WorldgenRandom,
768 ) -> Option<BlockPos> {
769 let mut sculk_position = pos;
770 for offset in Self::sculk_randomized_non_corner_neighbor_offsets(random) {
771 let neighbor = pos.offset(offset.x(), offset.y(), offset.z());
772 let transferee = region.block_state(neighbor);
773 if matches!(Self::sculk_behavior(transferee), SculkBehaviorKind::Default)
774 || !Self::sculk_is_movement_unobstructed(region, pos, neighbor)
775 {
776 continue;
777 }
778
779 sculk_position = neighbor;
780 if Self::sculk_vein_has_substrate_access(region, transferee, neighbor) {
781 break;
782 }
783 }
784
785 if sculk_position == pos {
786 None
787 } else {
788 Some(sculk_position)
789 }
790 }
791
792 fn sculk_randomized_non_corner_neighbor_offsets(random: &mut WorldgenRandom) -> Vec<BlockPos> {
793 let mut offsets = Vec::with_capacity(18);
794 for z in -1..=1 {
795 for y in -1..=1 {
796 for x in -1..=1 {
797 if (x == 0 || y == 0 || z == 0) && (x != 0 || y != 0 || z != 0) {
798 offsets.push(BlockPos::new(x, y, z));
799 }
800 }
801 }
802 }
803
804 for i in (1..offsets.len()).rev() {
805 let Ok(bound) = i32::try_from(i + 1) else {
806 panic!("sculk neighbor offset count exceeds i32 range");
807 };
808 let j = random.next_i32_bounded(bound) as usize;
809 offsets.swap(i, j);
810 }
811 offsets
812 }
813
814 fn sculk_is_movement_unobstructed(
815 region: &WorldGenRegion<'_>,
816 from: BlockPos,
817 to: BlockPos,
818 ) -> bool {
819 if Self::manhattan_distance(from, to) == 1 {
820 return true;
821 }
822
823 let dx = to.x() - from.x();
824 let dy = to.y() - from.y();
825 let dz = to.z() - from.z();
826 let direction_x = Self::sculk_direction_from_axis_delta(Axis::X, dx);
827 let direction_y = Self::sculk_direction_from_axis_delta(Axis::Y, dy);
828 let direction_z = Self::sculk_direction_from_axis_delta(Axis::Z, dz);
829 if dx == 0 {
830 Self::sculk_is_unobstructed(region, from, direction_y)
831 || Self::sculk_is_unobstructed(region, from, direction_z)
832 } else if dy == 0 {
833 Self::sculk_is_unobstructed(region, from, direction_x)
834 || Self::sculk_is_unobstructed(region, from, direction_z)
835 } else {
836 Self::sculk_is_unobstructed(region, from, direction_x)
837 || Self::sculk_is_unobstructed(region, from, direction_y)
838 }
839 }
840
841 fn sculk_is_unobstructed(
842 region: &WorldGenRegion<'_>,
843 from: BlockPos,
844 direction: Direction,
845 ) -> bool {
846 let test_pos = from.relative(direction);
847 !region
848 .block_state(test_pos)
849 .is_face_sturdy_at(test_pos, direction.opposite())
850 }
851
852 const fn sculk_direction_from_axis_delta(axis: Axis, delta: i32) -> Direction {
853 match (axis, delta < 0) {
854 (Axis::X, true) => Direction::West,
855 (Axis::X, false) => Direction::East,
856 (Axis::Y, true) => Direction::Down,
857 (Axis::Y, false) => Direction::Up,
858 (Axis::Z, true) => Direction::North,
859 (Axis::Z, false) => Direction::South,
860 }
861 }
862
863 fn sculk_vein_has_substrate_access(
864 region: &WorldGenRegion<'_>,
865
866 state: BlockStateId,
867 pos: BlockPos,
868 ) -> bool {
869 if state.get_block() != &vanilla_blocks::SCULK_VEIN {
870 return false;
871 }
872
873 Self::VANILLA_DIRECTION_VALUES.iter().any(|&direction| {
874 Self::sculk_vein_has_face(state, direction)
875 && region
876 .block_state(pos.relative(direction))
877 .get_block()
878 .has_tag(&BlockTag::SCULK_REPLACEABLE)
879 })
880 }
881
882 fn sculk_can_place_growth(region: &WorldGenRegion<'_>, pos: BlockPos) -> bool {
883 let above = pos.above();
884 let state_above = region.block_state(above);
885 if !state_above.is_air()
886 && (state_above.get_block() != &vanilla_blocks::WATER
887 || !get_fluid_state_from_block(state_above).is_water())
888 {
889 return false;
890 }
891
892 let mut growth_count = 0;
893 for z in -4..=4 {
894 for y in 0..=2 {
895 for x in -4..=4 {
896 let state = region.block_state(pos.offset(x, y, z));
897 if state.get_block() == &vanilla_blocks::SCULK_SENSOR
898 || state.get_block() == &vanilla_blocks::SCULK_SHRIEKER
899 {
900 growth_count += 1;
901 }
902
903 if growth_count > 2 {
904 return false;
905 }
906 }
907 }
908 }
909
910 true
911 }
912
913 fn sculk_random_growth_state(
914 region: &WorldGenRegion<'_>,
915 random: &mut WorldgenRandom,
916 pos: BlockPos,
917 is_world_generation: bool,
918 ) -> BlockStateId {
919 let state = if random.next_i32_bounded(11) == 0 {
920 vanilla_blocks::SCULK_SHRIEKER
921 .default_state()
922 .set_value(&BlockStateProperties::CAN_SUMMON, is_world_generation)
923 } else {
924 vanilla_blocks::SCULK_SENSOR.default_state()
925 };
926
927 if state
928 .try_get_value(&BlockStateProperties::WATERLOGGED)
929 .is_some()
930 && !get_fluid_state_from_block(region.block_state(pos)).is_empty()
931 {
932 state.set_value(&BlockStateProperties::WATERLOGGED, true)
933 } else {
934 state
935 }
936 }
937
938 fn set_sculk_growth_block_entity(
939 region: &WorldGenRegion<'_>,
940 pos: BlockPos,
941 state: BlockStateId,
942 ) {
943 if state.get_block() == &vanilla_blocks::SCULK_SENSOR {
944 Self::set_empty_block_entity(
945 region,
946 pos,
947 &vanilla_block_entity_types::SCULK_SENSOR,
948 state,
949 );
950 } else if state.get_block() == &vanilla_blocks::SCULK_SHRIEKER {
951 Self::set_empty_block_entity(
952 region,
953 pos,
954 &vanilla_block_entity_types::SCULK_SHRIEKER,
955 state,
956 );
957 }
958 }
959
960 fn sculk_decay_penalty(
961 spreader: &SculkSpreader,
962 pos: BlockPos,
963 origin: BlockPos,
964 charge: i32,
965 ) -> i32 {
966 let no_growth_radius = spreader.no_growth_radius as f32;
967 let dx = (pos.x() - origin.x()) as f32;
968 let dy = (pos.y() - origin.y()) as f32;
969 let dz = (pos.z() - origin.z()) as f32;
970 let distance = (dx * dx + dy * dy + dz * dz).sqrt();
971 let outer_distance_squared = (distance - no_growth_radius) * (distance - no_growth_radius);
972 let max_reach = (24 - spreader.no_growth_radius) as f32;
973 let max_reach_squared = max_reach * max_reach;
974 let distance_factor = (outer_distance_squared / max_reach_squared).min(1.0);
975 1.max((charge as f32 * distance_factor * 0.5) as i32)
976 }
977
978 fn sculk_closer_than(pos: BlockPos, origin: BlockPos, radius: i32) -> bool {
979 let radius_squared = i64::from(radius) * i64::from(radius);
980 Self::sculk_distance_squared(pos, origin) < radius_squared
981 }
982
983 fn sculk_horizontal_close_to_origin(pos: BlockPos, origin: BlockPos, radius: f64) -> bool {
984 let dx = f64::from(pos.x() - origin.x());
985 let dz = f64::from(pos.z() - origin.z());
986 dx * dx + dz * dz < radius * radius
987 }
988
989 fn sculk_distance_squared(left: BlockPos, right: BlockPos) -> i64 {
990 let dx = i64::from(left.x()) - i64::from(right.x());
991 let dy = i64::from(left.y()) - i64::from(right.y());
992 let dz = i64::from(left.z()) - i64::from(right.z());
993 dx * dx + dy * dy + dz * dz
994 }
995
996 fn sculk_cursor_is_pos_unreasonable(pos: BlockPos, origin: BlockPos) -> bool {
997 Self::sculk_abs_diff(pos.x(), origin.x())
998 .max(Self::sculk_abs_diff(pos.y(), origin.y()))
999 .max(Self::sculk_abs_diff(pos.z(), origin.z()))
1000 > SculkChargeCursor::MAX_CURSOR_DISTANCE
1001 }
1002
1003 const fn sculk_abs_diff(left: i32, right: i32) -> i32 {
1004 if left >= right {
1005 left - right
1006 } else {
1007 right - left
1008 }
1009 }
1010
1011 fn sculk_update_decay_delay(behavior: SculkBehaviorKind, age: i32) -> i32 {
1012 match behavior {
1013 SculkBehaviorKind::Default => (age - 1).max(0),
1014 SculkBehaviorKind::Sculk | SculkBehaviorKind::SculkVein => 1,
1015 }
1016 }
1017
1018 const fn sculk_spread_delay(_behavior: SculkBehaviorKind) -> i32 {
1019 1
1020 }
1021
1022 const fn sculk_can_change_block_state_on_spread(behavior: SculkBehaviorKind) -> bool {
1023 !matches!(behavior, SculkBehaviorKind::Sculk)
1024 }
1025
1026 fn sculk_behavior(state: BlockStateId) -> SculkBehaviorKind {
1027 if state.get_block() == &vanilla_blocks::SCULK {
1028 SculkBehaviorKind::Sculk
1029 } else if state.get_block() == &vanilla_blocks::SCULK_VEIN {
1030 SculkBehaviorKind::SculkVein
1031 } else {
1032 SculkBehaviorKind::Default
1033 }
1034 }
1035
1036 fn sculk_available_faces(state: BlockStateId) -> Vec<Direction> {
1037 let mut faces = Vec::new();
1038 if state.get_block() != &vanilla_blocks::SCULK_VEIN {
1039 return faces;
1040 }
1041
1042 for direction in Self::VANILLA_DIRECTION_VALUES {
1043 if Self::sculk_vein_has_face(state, direction) {
1044 faces.push(direction);
1045 }
1046 }
1047 faces
1048 }
1049
1050 fn sculk_vein_can_spread_from(state: BlockStateId, face: Direction) -> bool {
1051 Self::sculk_vein_is_other_block_valid_as_source(state)
1052 || Self::sculk_vein_has_face(state, face)
1053 }
1054
1055 fn sculk_vein_is_other_block_valid_as_source(state: BlockStateId) -> bool {
1056 state.get_block() != &vanilla_blocks::SCULK_VEIN
1057 }
1058
1059 fn sculk_vein_spread_pos(
1060 pos: BlockPos,
1061 spread_direction: Direction,
1062 from_face: Direction,
1063 spread_type: SculkSpreadType,
1064 ) -> SculkSpreadPos {
1065 match spread_type {
1066 SculkSpreadType::SamePosition => SculkSpreadPos {
1067 pos,
1068 face: spread_direction,
1069 },
1070 SculkSpreadType::SamePlane => SculkSpreadPos {
1071 pos: pos.relative(spread_direction),
1072 face: from_face,
1073 },
1074 SculkSpreadType::WrapAround => SculkSpreadPos {
1075 pos: pos.relative(spread_direction).relative(from_face),
1076 face: spread_direction.opposite(),
1077 },
1078 }
1079 }
1080
1081 fn sculk_vein_has_any_face(state: BlockStateId) -> bool {
1082 Self::VANILLA_DIRECTION_VALUES
1083 .iter()
1084 .any(|&direction| Self::sculk_vein_has_face(state, direction))
1085 }
1086
1087 fn sculk_vein_has_face(state: BlockStateId, direction: Direction) -> bool {
1088 state
1089 .try_get_value(Self::sculk_vein_face_property(direction))
1090 .unwrap_or(false)
1091 }
1092
1093 const fn sculk_vein_face_property(direction: Direction) -> &'static BoolProperty {
1094 match direction {
1095 Direction::Up => &BlockStateProperties::UP,
1096 Direction::Down => &BlockStateProperties::DOWN,
1097 Direction::North => &BlockStateProperties::NORTH,
1098 Direction::South => &BlockStateProperties::SOUTH,
1099 Direction::East => &BlockStateProperties::EAST,
1100 Direction::West => &BlockStateProperties::WEST,
1101 }
1102 }
1103}