Skip to main content

veloren_world/site/plot/
farm_field.rs

1use super::*;
2use crate::{ColumnSample, Land};
3use common::terrain::{
4    Block, BlockKind, SpriteKind,
5    sprite::{Owned, RelativeNeighborPosition},
6};
7use rand::{prelude::*, seq::IndexedRandom};
8use strum::{EnumIter, IntoEnumIterator};
9use vek::*;
10
11#[derive(EnumIter)]
12enum Crop {
13    Wildflower,
14    Wheat,
15    Flax,
16    Corn,
17    Tomato,
18    Carrot,
19    Radish,
20    Turnip,
21    Lettuce,
22    Pumpkin,
23    Sunflower,
24    Cactus,
25}
26
27impl Crop {
28    // None => no rows
29    // Some((row width, row coverage proportion)) =>
30    fn row_spacing(&self) -> Option<(f32, f32)> {
31        match self {
32            Self::Wildflower => None,
33            // Grains
34            Self::Wheat | Self::Flax | Self::Corn => Some((6.0, 0.8)),
35            // Bushes
36            Self::Tomato | Self::Cactus => Some((3.0, 1.0 / 3.0)),
37            // Root & brassica
38            Self::Carrot | Self::Radish | Self::Turnip | Self::Lettuce | Self::Pumpkin => {
39                Some((6.0, 0.75))
40            },
41            Self::Sunflower => Some((4.0, 0.5)),
42        }
43    }
44
45    fn sprites(&self) -> &[(f32, Option<SpriteKind>)] {
46        match self {
47            Self::Wheat => &[
48                (4.0, Some(SpriteKind::Empty)),
49                (1.0, Some(SpriteKind::WheatGreen)),
50                (1.0, Some(SpriteKind::WheatYellow)),
51            ],
52            Self::Flax => &[
53                (4.0, Some(SpriteKind::Empty)),
54                (1.0, Some(SpriteKind::Flax)),
55            ],
56            Self::Corn => &[
57                (4.0, Some(SpriteKind::Empty)),
58                (1.0, Some(SpriteKind::Corn)),
59            ],
60            Self::Wildflower => &[
61                (40.0, None),
62                (1.0, Some(SpriteKind::BlueFlower)),
63                (1.0, Some(SpriteKind::PinkFlower)),
64                (1.0, Some(SpriteKind::PurpleFlower)),
65                (0.1, Some(SpriteKind::RedFlower)),
66                (1.0, Some(SpriteKind::WhiteFlower)),
67                (1.0, Some(SpriteKind::YellowFlower)),
68                (1.0, Some(SpriteKind::Sunflower)),
69                (4.0, Some(SpriteKind::LongGrass)),
70                (4.0, Some(SpriteKind::MediumGrass)),
71                (4.0, Some(SpriteKind::ShortGrass)),
72            ],
73            Self::Tomato => &[
74                (1.5, Some(SpriteKind::Empty)),
75                (1.0, Some(SpriteKind::Tomato)),
76            ],
77            Self::Carrot => &[
78                (5.0, Some(SpriteKind::Empty)),
79                (1.0, Some(SpriteKind::Carrot)),
80            ],
81            Self::Radish => &[
82                (5.0, Some(SpriteKind::Empty)),
83                (1.0, Some(SpriteKind::Radish)),
84            ],
85            Self::Turnip => &[
86                (5.0, Some(SpriteKind::Empty)),
87                (1.0, Some(SpriteKind::Turnip)),
88            ],
89            Self::Lettuce => &[
90                (5.0, Some(SpriteKind::Empty)),
91                (1.0, Some(SpriteKind::Lettuce)),
92            ],
93            Self::Pumpkin => &[
94                (5.0, Some(SpriteKind::Empty)),
95                (1.0, Some(SpriteKind::Pumpkin)),
96            ],
97            Self::Sunflower => &[
98                (5.0, Some(SpriteKind::Empty)),
99                (1.0, Some(SpriteKind::Sunflower)),
100            ],
101            Self::Cactus => &[
102                (10.0, Some(SpriteKind::Empty)),
103                (1.0, Some(SpriteKind::BarrelCactus)),
104                (1.0, Some(SpriteKind::RoundCactus)),
105                (1.0, Some(SpriteKind::ShortCactus)),
106                (1.0, Some(SpriteKind::MedFlatCactus)),
107                (1.0, Some(SpriteKind::ShortFlatCactus)),
108                (1.0, Some(SpriteKind::LargeCactus)),
109                (1.0, Some(SpriteKind::TallCactus)),
110            ],
111        }
112    }
113}
114
115/// Represents house data generated by the `generate()` method
116pub struct FarmField {
117    crop: Crop,
118    /// Axis aligned bounding region for the house
119    bounds: Aabr<i32>,
120    /// Approximate altitude of the door tile
121    pub(crate) alt: i32,
122    ori: Vec2<f32>,
123    is_desert: bool,
124    step_size: Option<f32>,
125}
126
127impl FarmField {
128    pub fn generate(
129        land: &Land,
130        rng: &mut impl Rng,
131        site: &Site,
132        door_tile: Vec2<i32>,
133        door_dir: Vec2<i32>,
134        tile_aabr: Aabr<i32>,
135        is_desert: bool,
136    ) -> Self {
137        let bounds = Aabr {
138            min: site.tile_wpos(tile_aabr.min),
139            max: site.tile_wpos(tile_aabr.max),
140        };
141
142        let crop = if is_desert {
143            Crop::Cactus
144        } else {
145            Crop::iter()
146                .filter(|crop| !matches!(crop, Crop::Cactus))
147                .choose(rng)
148                .unwrap()
149        };
150
151        // Stepped terrain, but only on slopes
152        let center_grad = land.get_gradient_approx(bounds.center());
153        let step_size = if center_grad > 0.25 {
154            Some((center_grad * 8.0).clamped(3.0, 12.0))
155        } else {
156            None
157        };
158
159        Self {
160            bounds,
161            alt: land.get_alt_approx(site.tile_center_wpos(door_tile + door_dir)) as i32,
162            ori: {
163                let norm = land
164                    .get_approx_chunk_terrain_normal(bounds.center())
165                    .unwrap_or_default();
166                let ori = rng.random_range(0.0..std::f32::consts::TAU);
167                norm.xy() * 5.0 + Vec2::new(ori.sin(), ori.cos())
168            },
169            crop,
170            is_desert,
171            step_size,
172        }
173    }
174
175    /// Returns the properties of banked steps for this field (fract, alt)
176    fn step(&self, alt: f32) -> Option<(f32, f32)> {
177        const STEP_POW: i32 = 8;
178
179        if let Some(step_size) = self.step_size {
180            let step_alt = alt / step_size;
181            let step_fract = step_alt.fract();
182            let h_fract = if step_fract < 0.5 {
183                (step_fract * 2.0).powi(STEP_POW) * 0.5
184            } else {
185                1.0 - ((1.0 - step_fract) * 2.0).powi(STEP_POW) * 0.5
186            };
187            Some((step_fract, (step_alt.floor() + h_fract) * step_size))
188        } else {
189            None
190        }
191    }
192}
193
194impl Structure for FarmField {
195    #[cfg(feature = "dyn-lib")]
196    #[unsafe(export_name = "as_dyn_structure_farmfield")]
197    fn as_dyn_outer(&self) -> Option<(&dyn Structure, &'static str)> {
198        Some((Self::as_dyn_impl(self), "as_dyn_structure_farmfield"))
199    }
200
201    fn spawn_rules_inner(
202        &self,
203        spawn_rules: &mut SpawnRules,
204        land: &Land,
205        wpos: Vec2<i32>,
206        weight: f32,
207    ) {
208        if let Some((_, step_alt)) = self.step(land.get_alt_approx(wpos)) {
209            spawn_rules.prefer_alt(step_alt, weight);
210        }
211    }
212
213    fn terrain_surface_at_inner(
214        &self,
215        wpos: Vec2<i32>,
216        old: Block,
217        rng: &mut ChaCha8Rng,
218        col: &ColumnSample,
219        z_off: i32,
220        _site: &Site,
221    ) -> Option<Block> {
222        let t = (self.ori.yx() * wpos.as_()).magnitude();
223
224        // Don't grow crops on steep terrain
225        let is_bank = self
226            .step(col.alt)
227            .is_some_and(|(step_fract, _)| (step_fract - 0.5).abs() < 0.45);
228
229        let is_trench = self
230            .crop
231            .row_spacing()
232            .map(|(w, p)| (t / w).fract() <= p)
233            .unwrap_or(false);
234
235        let hit_min_x_bounds = wpos.x == self.bounds.min.x;
236        let hit_min_y_bounds = wpos.y == self.bounds.min.y;
237        let hit_max_x_bounds = wpos.x == self.bounds.max.x - 1;
238        let hit_max_y_bounds = wpos.y == self.bounds.max.y - 1;
239
240        let is_bounds =
241            hit_min_x_bounds || hit_min_y_bounds || hit_max_x_bounds || hit_max_y_bounds;
242
243        let is_corner = (hit_max_y_bounds || hit_min_y_bounds)
244            && (hit_max_x_bounds || hit_min_x_bounds)
245            && is_bounds;
246
247        if z_off == 1 && is_bounds {
248            // fence
249            let adjacent_type = if is_corner {
250                RelativeNeighborPosition::L
251            } else {
252                RelativeNeighborPosition::I
253            };
254
255            let ori = if !is_corner {
256                // for straight - "I"
257                // can only go in the vertical or horizontal direction
258                if hit_min_x_bounds || hit_max_x_bounds {
259                    2
260                } else {
261                    0
262                }
263            } else {
264                // for corners - "L"
265                // can be rotated in 4 different directions
266                if hit_min_x_bounds && hit_min_y_bounds {
267                    4
268                } else if hit_max_x_bounds && hit_min_y_bounds {
269                    6
270                } else if hit_min_x_bounds && hit_max_y_bounds {
271                    2
272                } else {
273                    0
274                }
275            };
276
277            Some(
278                old.into_vacant()
279                    .with_sprite(SpriteKind::FenceWoodWoodland)
280                    .with_ori(ori)
281                    .unwrap()
282                    .with_adjacent_type(adjacent_type)
283                    .unwrap(),
284            )
285        } else if is_bank {
286            // Don't grow anything on banks
287            None
288        } else if matches!(old.kind(), BlockKind::Grass | BlockKind::Sand) {
289            // soil
290            Some(Block::new(
291                if self.is_desert {
292                    BlockKind::Sand
293                } else {
294                    BlockKind::Grass
295                },
296                (Lerp::lerp(
297                    col.surface_color,
298                    col.sub_surface_color * 0.5,
299                    is_trench as i32 as f32,
300                ) * 255.0)
301                    .as_(),
302            ))
303        } else if z_off == 1 && (is_trench || self.crop.row_spacing().is_none()) {
304            // crops
305            self.crop
306                .sprites()
307                .choose_weighted(rng, |(w, _)| *w)
308                .ok()
309                .and_then(|&(_, s)| {
310                    let new = old.into_vacant().with_sprite(s?);
311                    let new = new.with_attr(Owned(true)).unwrap_or(new);
312
313                    Some(new)
314                })
315        } else if z_off == 1 && rng.random_bool(0.001) {
316            Some(old.into_vacant().with_sprite(SpriteKind::Scarecrow))
317        } else {
318            None
319        }
320    }
321}