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