Skip to main content

steel_core/worldgen/feature/features/
sculk_patch.rs

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}