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