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