Skip to main content

steel_core/worldgen/carver/
canyon.rs

1//! Canyon (ravine) carver.
2//!
3//! Carves a single long, narrow tunnel per chunk with per-height width
4//! variation; runs only from an overworld biome's carver list (`minecraft:canyon`, 1 % probability).
5
6use std::f32::consts::{PI, TAU};
7
8use steel_math::trig;
9use steel_registry::carver::CanyonCarverConfiguration;
10use steel_utils::random::{Random, legacy_random::LegacyRandom};
11use steel_utils::{BlockPos, ChunkPos};
12use steel_worldgen::density::DimensionNoises;
13
14use crate::worldgen::carver::{CarveRun, can_reach, horizontal_tunnel_radius};
15
16/// Vanilla `WorldCarver.getRange()` — 4 chunks each direction. Shared with
17/// the cave carver.
18const CARVER_RANGE: i32 = 4;
19const MAX_TUNNEL_DISTANCE: i32 = (CARVER_RANGE * 2 - 1) * 16;
20
21/// Position + rotation state that evolves along the canyon's length.
22#[derive(Debug, Clone, Copy)]
23struct CanyonState {
24    x: f64,
25    y: f64,
26    z: f64,
27    /// Yaw.
28    horizontal_rotation: f32,
29    /// Pitch.
30    vertical_rotation: f32,
31}
32
33/// Static per-tunnel parameters for the canyon's `do_carve` loop.
34#[derive(Debug, Clone, Copy)]
35struct CanyonTunnel {
36    tunnel_seed: i64,
37    thickness: f32,
38    distance: i32,
39    y_scale: f64,
40}
41
42impl<N, F> CarveRun<'_, '_, N, F>
43where
44    N: DimensionNoises,
45    F: FnMut(BlockPos) -> u16,
46{
47    /// Runs a canyon carver pass rooted in `source_pos`. `random` must have
48    /// been seeded by the caller via
49    /// `set_large_feature_seed(seed + carver_index, source.x, source.z)`
50    /// and the `isStartChunk` probability check must have already passed.
51    ///
52    /// Mirrors vanilla's `CanyonWorldCarver.carve` — one tunnel per chunk,
53    /// no splits, no rooms.
54    pub fn carve_canyon(
55        &mut self,
56        config: &CanyonCarverConfiguration,
57        source_pos: ChunkPos,
58        random: &mut LegacyRandom,
59    ) {
60        let source_min_x = source_pos.0.x * 16;
61        let source_min_z = source_pos.0.y * 16;
62
63        let state = CanyonState {
64            x: f64::from(source_min_x + random.next_i32_bounded(16)),
65            y: f64::from(config.base.y.sample(
66                random,
67                self.ctx.min_y,
68                self.ctx.gen_depth,
69                self.ctx.sea_level,
70            )),
71            z: f64::from(source_min_z + random.next_i32_bounded(16)),
72            horizontal_rotation: random.next_f32() * TAU,
73            vertical_rotation: config.vertical_rotation.sample(random),
74        };
75
76        // Draw order: yScale, thickness, distance_factor→distance, tunnel_seed.
77        let y_scale = f64::from(config.shape.y_scale.sample(random));
78        let thickness = config.shape.thickness.sample(random);
79        let distance =
80            (MAX_TUNNEL_DISTANCE as f32 * config.shape.distance_factor.sample(random)) as i32;
81        let tunnel_seed = random.next_i64();
82
83        let tunnel = CanyonTunnel {
84            tunnel_seed,
85            thickness,
86            distance,
87            y_scale,
88        };
89
90        self.do_carve_canyon(config, state, tunnel);
91    }
92
93    fn do_carve_canyon(
94        &mut self,
95        config: &CanyonCarverConfiguration,
96        mut state: CanyonState,
97        tunnel: CanyonTunnel,
98    ) {
99        let mut random = LegacyRandom::from_seed(tunnel.tunnel_seed as u64);
100        let width_factors = config
101            .shape
102            .init_width_factors(self.ctx.gen_depth, &mut random);
103        let mut y_rota: f32 = 0.0;
104        let mut x_rota: f32 = 0.0;
105
106        for current_step in 0..tunnel.distance {
107            let progress = PI * current_step as f32 / tunnel.distance as f32;
108            let mut horizontal_radius = horizontal_tunnel_radius(progress, tunnel.thickness);
109            let mut vertical_radius = horizontal_radius * tunnel.y_scale;
110            horizontal_radius *=
111                f64::from(config.shape.horizontal_radius_factor.sample(&mut random));
112            vertical_radius = config.shape.update_vertical_radius(
113                &mut random,
114                vertical_radius,
115                tunnel.distance as f32,
116                current_step as f32,
117            );
118
119            let xc = trig::cos(state.vertical_rotation);
120            let xs = trig::sin(state.vertical_rotation);
121            state.x += f64::from(trig::cos(state.horizontal_rotation) * xc);
122            state.y += f64::from(xs);
123            state.z += f64::from(trig::sin(state.horizontal_rotation) * xc);
124            state.vertical_rotation *= 0.7;
125            state.vertical_rotation += x_rota * 0.05;
126            state.horizontal_rotation += y_rota * 0.05;
127            x_rota *= 0.8;
128            y_rota *= 0.5;
129            x_rota += (random.next_f32() - random.next_f32()) * random.next_f32() * 2.0;
130            y_rota += (random.next_f32() - random.next_f32()) * random.next_f32() * 4.0;
131
132            if random.next_i32_bounded(4) == 0 {
133                continue;
134            }
135
136            if !can_reach(
137                self.chunk_min_x,
138                self.chunk_min_z,
139                state.x,
140                state.z,
141                current_step,
142                tunnel.distance,
143                tunnel.thickness,
144            ) {
145                return;
146            }
147
148            let min_y = self.ctx.min_y;
149            let skip_checker = |xd: f64, yd: f64, zd: f64, world_y: i32| {
150                should_skip_canyon(&width_factors, min_y, xd, yd, zd, world_y)
151            };
152
153            self.carve_ellipsoid(
154                state.x,
155                state.y,
156                state.z,
157                horizontal_radius,
158                vertical_radius,
159                skip_checker,
160            );
161        }
162    }
163}
164
165/// Vanilla `CanyonWorldCarver.shouldSkip`. Vanilla indexes
166/// `widthFactorPerHeight[yIndex - 1]` where `yIndex = world_y -
167/// context.getMinGenY()`; i.e. the previous Y's width factor applied to this
168/// block's radial test.
169fn should_skip_canyon(
170    width_factors: &[f32],
171    min_y: i32,
172    xd: f64,
173    yd: f64,
174    zd: f64,
175    world_y: i32,
176) -> bool {
177    let y_index = (world_y - min_y - 1) as usize;
178    let factor = width_factors[y_index];
179    (xd * xd + zd * zd) * f64::from(factor) + yd * yd / 6.0 >= 1.0
180}