Skip to main content

veloren_voxygen/scene/terrain/
watcher.rs

1use crate::hud::CraftingTab;
2use common::{
3    terrain::{Block, BlockKind, SpriteKind, sprite},
4    vol::ReadVol,
5};
6use common_base::span;
7use rand::prelude::*;
8use rand_chacha::ChaCha8Rng;
9use vek::*;
10
11#[derive(Clone, Copy, Debug)]
12pub enum Interaction {
13    /// This covers mining, unlocking, and regular collectable things (e.g.
14    /// twigs).
15    Collect,
16    Craft(CraftingTab),
17    Mount,
18    Read,
19    LightToggle(bool),
20}
21
22#[derive(Copy, Clone)]
23pub enum FireplaceType {
24    House,
25    Workshop, // this also includes witch hut
26}
27
28pub struct SmokerProperties {
29    pub position: Vec3<i32>,
30    pub kind: FireplaceType,
31}
32
33impl SmokerProperties {
34    fn new(position: Vec3<i32>, kind: FireplaceType) -> Self { Self { position, kind } }
35}
36
37#[derive(Default)]
38pub struct BlocksOfInterest {
39    pub leaves: Vec<Vec3<i32>>,
40    pub water: Vec<Vec3<i32>>,
41    pub cave_roof: Vec<Vec3<i32>>,
42    pub drip: Vec<Vec3<i32>>,
43    pub grass: Vec<Vec3<i32>>,
44    pub slow_river: Vec<Vec3<i32>>,
45    pub waterfall: Vec<(Vec3<i32>, Vec3<f32>)>,
46    pub lavapool: Vec<Vec3<i32>>,
47    pub fires: Vec<Vec3<i32>>,
48    pub smokers: Vec<SmokerProperties>,
49    pub beehives: Vec<Vec3<i32>>,
50    pub reeds: Vec<Vec3<i32>>,
51    pub fireflies: Vec<Vec3<i32>>,
52    pub flowers: Vec<Vec3<i32>>,
53    pub fire_bowls: Vec<Vec3<i32>>,
54    pub snow: Vec<Vec3<i32>>,
55    pub spores: Vec<Vec3<i32>>,
56    //This is so crickets stay in place and don't randomly change sounds
57    pub cricket1: Vec<Vec3<i32>>,
58    pub cricket2: Vec<Vec3<i32>>,
59    pub cricket3: Vec<Vec3<i32>>,
60    pub frogs: Vec<Vec3<i32>>,
61    pub one_way_walls: Vec<(Vec3<i32>, Vec3<f32>)>,
62    // Note: these are only needed for chunks within the iteraction range so this is a potential
63    // area for optimization
64    pub interactables: Vec<(Vec3<i32>, Interaction)>,
65    pub lights: Vec<(Vec3<i32>, u8)>,
66    pub train_smokes: Vec<Vec3<i32>>,
67    // needed for biome specific smoke variations
68    pub temperature: f32,
69    pub humidity: f32,
70}
71
72impl BlocksOfInterest {
73    pub fn from_blocks(
74        blocks: impl Iterator<Item = (Vec3<i32>, Block)>,
75        river_velocity: Vec3<f32>,
76        temperature: f32,
77        humidity: f32,
78        chunk: &impl ReadVol<Vox = Block>,
79    ) -> Self {
80        span!(_guard, "from_chunk", "BlocksOfInterest::from_chunk");
81        let mut leaves = Vec::new();
82        let mut water = Vec::new();
83        let mut cave_roof = Vec::new();
84        let mut drip = Vec::new();
85        let mut grass = Vec::new();
86        let mut slow_river = Vec::new();
87        let mut waterfall = Vec::new();
88        let mut lavapool = Vec::new();
89        let mut fires = Vec::new();
90        let mut smokers = Vec::new();
91        let mut beehives = Vec::new();
92        let mut reeds = Vec::new();
93        let mut fireflies = Vec::new();
94        let mut flowers = Vec::new();
95        let mut interactables = Vec::new();
96        let mut lights = Vec::new();
97        // Lights that can be omitted at random if we have too many and need to cull
98        // some of them
99        let mut minor_lights = Vec::new();
100        let mut fire_bowls = Vec::new();
101        let mut snow = Vec::new();
102        let mut cricket1 = Vec::new();
103        let mut cricket2 = Vec::new();
104        let mut cricket3 = Vec::new();
105        let mut frogs = Vec::new();
106        let mut one_way_walls = Vec::new();
107        let mut spores = Vec::new();
108        let mut train_smokes = Vec::new();
109
110        let mut rng = ChaCha8Rng::from_seed(rand::rng().random());
111
112        blocks.for_each(|(pos, block)| {
113            match block.kind() {
114                BlockKind::Leaves
115                    if rng.random_range(0..16) == 0
116                        && chunk
117                            .get(pos - Vec3::unit_z())
118                            .map_or(true, |b| !b.is_filled()) =>
119                {
120                    leaves.push(pos)
121                },
122                BlockKind::Water
123                    if rng.random_range(0..8) == 0
124                        && chunk
125                            .get(pos + Vec3::unit_z())
126                            .map_or(true, |b| b.kind() == BlockKind::Air) =>
127                {
128                    water.push(pos)
129                },
130                BlockKind::Rock | BlockKind::Sand
131                    if rng.random_range(0..4) == 0
132                        && chunk
133                            .get(pos - Vec3::unit_z())
134                            .map_or(true, |b| !b.is_filled()) =>
135                {
136                    cave_roof.push(pos)
137                },
138                BlockKind::WeakRock if rng.random_range(0..6) == 0 => drip.push(pos),
139                BlockKind::Grass => {
140                    if rng.random_range(0..16) == 0 {
141                        grass.push(pos);
142                    }
143                    match rng.random_range(0..8192) {
144                        1 => cricket1.push(pos),
145                        2 => cricket2.push(pos),
146                        3 => cricket3.push(pos),
147                        _ => {},
148                    }
149                },
150                BlockKind::Water => {
151                    let is_waterfall = chunk
152                        .get(pos + vek::Vec3::unit_z())
153                        .is_ok_and(|b| b.is_air())
154                        && [
155                            vek::Vec2::new(0, 1),
156                            vek::Vec2::new(1, 0),
157                            vek::Vec2::new(0, -1),
158                            vek::Vec2::new(-1, 0),
159                        ]
160                        .iter()
161                        .map(|p| {
162                            (1..=2)
163                                .take_while(|i| {
164                                    chunk.get(pos + p.with_z(*i)).is_ok_and(|b| b.is_liquid())
165                                })
166                                .count()
167                        })
168                        .any(|s| s >= 2);
169
170                    if is_waterfall {
171                        waterfall.push((pos, river_velocity));
172                    }
173
174                    let river_speed_sq = river_velocity.magnitude_squared();
175                    // Assign a river speed to water blocks depending on river velocity
176                    if river_speed_sq < 0.45 && river_speed_sq > 0.15 {
177                        slow_river.push(pos)
178                    }
179                },
180                BlockKind::Snow if rng.random_range(0..16) == 0 => snow.push(pos),
181                BlockKind::Lava
182                    if chunk
183                        .get(pos + Vec3::unit_z())
184                        .map_or(true, |b| !b.is_filled()) =>
185                {
186                    if rng.random_range(0..5) == 0 {
187                        fires.push(pos + Vec3::unit_z())
188                    }
189                    if rng.random_range(0..16) == 0 {
190                        lavapool.push(pos)
191                    }
192                },
193                BlockKind::GlowingMushroom if rng.random_range(0..8) == 0 => spores.push(pos),
194                BlockKind::Snow | BlockKind::Ice if rng.random_range(0..16) == 0 => snow.push(pos),
195                _ => {
196                    if let Some(sprite) = block.get_sprite() {
197                        if sprite.category() == sprite::Category::Lamp
198                            && let Ok(sprite::LightEnabled(enabled)) = block.get_attr()
199                        {
200                            interactables.push((pos, Interaction::LightToggle(!enabled)));
201                        }
202
203                        if block.is_mountable() {
204                            interactables.push((pos, Interaction::Mount));
205                        }
206
207                        match sprite {
208                            SpriteKind::Ember => {
209                                fires.push(pos);
210                                smokers.push(SmokerProperties::new(pos, FireplaceType::House));
211                            },
212                            SpriteKind::TrainSmoke => {
213                                train_smokes.push(pos);
214                            },
215                            SpriteKind::FireBlock => {
216                                fire_bowls.push(pos);
217                            },
218                            // Offset positions to account for block height.
219                            // TODO: Is this a good idea?
220                            SpriteKind::StreetLamp => fire_bowls.push(pos + Vec3::unit_z() * 2),
221                            SpriteKind::FireBowlGround => fire_bowls.push(pos + Vec3::unit_z()),
222                            SpriteKind::StreetLampTall => fire_bowls.push(pos + Vec3::unit_z() * 4),
223                            SpriteKind::WallSconce => fire_bowls.push(pos + Vec3::unit_z()),
224                            SpriteKind::Beehive => beehives.push(pos),
225                            SpriteKind::Reed => {
226                                reeds.push(pos);
227                                fireflies.push(pos);
228                                if rng.random_range(0..12) == 0 {
229                                    frogs.push(pos);
230                                }
231                            },
232                            SpriteKind::CaveMushroom => fireflies.push(pos),
233                            SpriteKind::PinkFlower => flowers.push(pos),
234                            SpriteKind::PurpleFlower => flowers.push(pos),
235                            SpriteKind::RedFlower => flowers.push(pos),
236                            SpriteKind::WhiteFlower => flowers.push(pos),
237                            SpriteKind::YellowFlower => flowers.push(pos),
238                            SpriteKind::Sunflower => flowers.push(pos),
239                            SpriteKind::CraftingBench => {
240                                interactables.push((pos, Interaction::Craft(CraftingTab::All)))
241                            },
242                            SpriteKind::SmokeDummy => {
243                                smokers.push(SmokerProperties::new(pos, FireplaceType::Workshop));
244                            },
245                            SpriteKind::Forge => interactables
246                                .push((pos, Interaction::Craft(CraftingTab::ProcessedMaterial))),
247                            SpriteKind::TanningRack => interactables
248                                .push((pos, Interaction::Craft(CraftingTab::ProcessedMaterial))),
249                            SpriteKind::SpinningWheel => {
250                                interactables.push((pos, Interaction::Craft(CraftingTab::All)))
251                            },
252                            SpriteKind::Loom => {
253                                interactables.push((pos, Interaction::Craft(CraftingTab::All)))
254                            },
255                            SpriteKind::Cauldron => {
256                                interactables.push((pos, Interaction::Craft(CraftingTab::Potion)))
257                            },
258                            SpriteKind::Anvil => {
259                                interactables.push((pos, Interaction::Craft(CraftingTab::Weapon)))
260                            },
261                            SpriteKind::CookingPot => {
262                                fires.push(pos);
263                                interactables.push((pos, Interaction::Craft(CraftingTab::Food)))
264                            },
265                            SpriteKind::DismantlingBench => interactables
266                                .push((pos, Interaction::Craft(CraftingTab::Dismantle))),
267                            SpriteKind::RepairBench => {
268                                interactables.push((pos, Interaction::Craft(CraftingTab::All)))
269                            },
270                            SpriteKind::OneWayWall => one_way_walls.push((
271                                pos,
272                                Vec2::unit_y()
273                                    .rotated_z(
274                                        std::f32::consts::PI
275                                            * 0.25
276                                            * block
277                                                .get_attr::<sprite::Ori>()
278                                                .unwrap_or(sprite::Ori(0))
279                                                .0
280                                                as f32,
281                                    )
282                                    .with_z(0.0),
283                            )),
284                            SpriteKind::Sign | SpriteKind::HangingSign => {
285                                interactables.push((pos, Interaction::Read))
286                            },
287                            SpriteKind::MycelBlue => spores.push(pos),
288                            SpriteKind::Mold => spores.push(pos),
289                            _ => {},
290                        }
291                    }
292                },
293            }
294            // NOTE: we don't care whether it requires mine-tool or not here.
295            if block.is_collectible() {
296                interactables.push((pos, Interaction::Collect));
297            }
298            if let Some(glow) = block.get_glow() {
299                // Currently, we count filled blocks as 'minor' lights, and sprites as
300                // non-minor.
301                if block.get_sprite().is_none() {
302                    minor_lights.push((pos, glow));
303                } else {
304                    lights.push((pos, glow));
305                }
306            }
307        });
308
309        // TODO: Come up with a better way to prune many light sources: grouping them
310        // into larger lights with k-means clustering, perhaps?
311        const MAX_MINOR_LIGHTS: usize = 64;
312        lights.extend(minor_lights.sample(&mut rng, MAX_MINOR_LIGHTS).copied());
313
314        Self {
315            leaves,
316            water,
317            cave_roof,
318            drip,
319            grass,
320            slow_river,
321            waterfall,
322            lavapool,
323            fires,
324            smokers,
325            beehives,
326            reeds,
327            fireflies,
328            flowers,
329            fire_bowls,
330            snow,
331            spores,
332            cricket1,
333            cricket2,
334            cricket3,
335            frogs,
336            one_way_walls,
337            interactables,
338            lights,
339            temperature,
340            humidity,
341            train_smokes,
342        }
343    }
344}