Skip to main content

steel_core/worldgen/carver/
canyon.rs

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