veloren_world/layer/
scatter.rs

1use crate::{
2    CONFIG, Canvas,
3    column::ColumnSample,
4    sim::SimChunk,
5    util::{RandomField, close},
6};
7use common::{
8    calendar::{Calendar, CalendarEvent},
9    terrain::{Block, BlockKind, SpriteKind, sprite::SnowCovered},
10};
11use noise::NoiseFn;
12use num::traits::Pow;
13use rand::prelude::*;
14use std::f32;
15use vek::*;
16
17/// Returns a decimal value between 0 and 1.
18/// The density is maximum at the middle of the highest and the lowest allowed
19/// altitudes, and zero otherwise. Quadratic curve.
20///
21/// The formula used is:
22///
23/// ```latex
24/// \max\left(-\frac{4\left(x-u\right)\left(x-l\right)}{\left(u-l\right)^{2}},\ 0\right)
25/// ```
26pub fn density_factor_by_altitude(lower_limit: f32, altitude: f32, upper_limit: f32) -> f32 {
27    let maximum: f32 = (upper_limit - lower_limit).pow(2) / 4.0f32;
28    (-((altitude - lower_limit) * (altitude - upper_limit)) / maximum).max(0.0)
29}
30
31const MUSH_FACT: f32 = 1.0e-4; // To balance things around the mushroom spawning rate
32const GRASS_FACT: f32 = 1.0e-3; // To balance things around the grass spawning rate
33const TREE_FACT: f32 = 0.15e-3; // To balance things around the wood spawning rate
34const DEPTH_WATER_NORM: f32 = 15.0; // Water depth at which regular underwater sprites start spawning
35pub fn apply_scatter_to(canvas: &mut Canvas, _rng: &mut impl Rng, calendar: Option<&Calendar>) {
36    enum WaterMode {
37        Underwater,
38        Floating,
39        Ground,
40    }
41    use WaterMode::*;
42
43    use SpriteKind::*;
44
45    struct ScatterConfig {
46        kind: SpriteKind,
47        water_mode: WaterMode,
48        permit: fn(BlockKind) -> bool,
49        f: fn(&SimChunk, &ColumnSample) -> (f32, Option<(f32, f32, f32)>),
50    }
51
52    // TODO: Add back all sprites we had before
53    let scatter: &[ScatterConfig] = &[
54        // (density, Option<(base_density_proportion, wavelen, threshold)>)
55        // Flowers
56        ScatterConfig {
57            kind: BlueFlower,
58            water_mode: Ground,
59            permit: |b| matches!(b, BlockKind::Grass),
60            f: |_, col| {
61                (
62                    close(col.temp, CONFIG.temperate_temp, 0.7).min(close(
63                        col.humidity,
64                        CONFIG.jungle_hum,
65                        0.4,
66                    )) * col.tree_density
67                        * MUSH_FACT
68                        * 256.0,
69                    Some((0.0, 256.0, 0.25)),
70                )
71            },
72        },
73        ScatterConfig {
74            kind: PinkFlower,
75            water_mode: Ground,
76            permit: |b| matches!(b, BlockKind::Grass),
77            f: |_, col| {
78                (
79                    close(col.temp, 0.0, 0.7).min(close(col.humidity, CONFIG.jungle_hum, 0.4))
80                        * col.tree_density
81                        * MUSH_FACT
82                        * 350.0,
83                    Some((0.0, 100.0, 0.1)),
84                )
85            },
86        },
87        ScatterConfig {
88            kind: PurpleFlower,
89            water_mode: Ground,
90            permit: |b| matches!(b, BlockKind::Grass | BlockKind::Snow),
91            f: |_, col| {
92                (
93                    close(col.temp, CONFIG.temperate_temp, 0.7)
94                        .max(close(col.temp, CONFIG.snow_temp, 0.7))
95                        .min(close(col.humidity, CONFIG.jungle_hum, 0.4).max(close(
96                            col.humidity,
97                            CONFIG.forest_hum,
98                            0.5,
99                        )))
100                        * col.tree_density
101                        * MUSH_FACT
102                        * 350.0,
103                    Some((0.0, 100.0, 0.1)),
104                )
105            },
106        },
107        ScatterConfig {
108            kind: RedFlower,
109            water_mode: Ground,
110            permit: |b| matches!(b, BlockKind::Grass | BlockKind::Snow),
111            f: |_, col| {
112                (
113                    close(col.temp, CONFIG.tropical_temp, 0.7)
114                        .max(close(col.temp, CONFIG.snow_temp, 0.7))
115                        .min(close(col.humidity, CONFIG.jungle_hum, 0.4).max(close(
116                            col.humidity,
117                            CONFIG.forest_hum,
118                            0.5,
119                        )))
120                        * col.tree_density
121                        * MUSH_FACT
122                        * 350.0,
123                    Some((0.0, 100.0, 0.1)),
124                )
125            },
126        },
127        ScatterConfig {
128            kind: WhiteFlower,
129            water_mode: Ground,
130            permit: |b| matches!(b, BlockKind::Grass),
131            f: |_, col| {
132                (
133                    close(col.temp, 0.0, 0.7).min(close(col.humidity, CONFIG.jungle_hum, 0.4))
134                        * col.tree_density
135                        * MUSH_FACT
136                        * 350.0,
137                    Some((0.0, 100.0, 0.1)),
138                )
139            },
140        },
141        ScatterConfig {
142            kind: YellowFlower,
143            water_mode: Ground,
144            permit: |b| matches!(b, BlockKind::Grass | BlockKind::Snow),
145            f: |_, col| {
146                (
147                    close(col.temp, 0.0, 0.7)
148                        .max(close(col.temp, CONFIG.snow_temp, 0.7))
149                        .min(close(col.humidity, CONFIG.jungle_hum, 0.4).max(close(
150                            col.humidity,
151                            CONFIG.forest_hum,
152                            0.5,
153                        )))
154                        * col.tree_density
155                        * MUSH_FACT
156                        * 350.0,
157                    Some((0.0, 100.0, 0.1)),
158                )
159            },
160        },
161        ScatterConfig {
162            kind: Cotton,
163            water_mode: Ground,
164            permit: |b| matches!(b, BlockKind::Earth | BlockKind::Grass),
165            f: |_, col| {
166                (
167                    close(col.temp, CONFIG.tropical_temp, 0.7).min(close(
168                        col.humidity,
169                        CONFIG.jungle_hum,
170                        0.4,
171                    )) * col.tree_density
172                    * MUSH_FACT
173                    * 200.0
174                    * (!col.snow_cover) as i32 as f32 /* To prevent spawning in snow covered areas */
175                    * density_factor_by_altitude(-500.0 , col.alt, 500.0), /* To prevent
176                                                                            * spawning at high
177                                                                            * altitudes */
178                    Some((0.0, 128.0, 0.30)),
179                )
180            },
181        },
182        ScatterConfig {
183            kind: Sunflower,
184            water_mode: Ground,
185            permit: |b| matches!(b, BlockKind::Grass),
186            f: |_, col| {
187                (
188                    close(col.temp, 0.0, 0.7).min(close(col.humidity, CONFIG.jungle_hum, 0.4))
189                        * col.tree_density
190                        * MUSH_FACT
191                        * 350.0,
192                    Some((0.0, 100.0, 0.15)),
193                )
194            },
195        },
196        ScatterConfig {
197            kind: WildFlax,
198            water_mode: Ground,
199            permit: |b| matches!(b, BlockKind::Grass),
200            f: |_, col| {
201                (
202                    close(col.temp, CONFIG.temperate_temp, 0.7).min(close(
203                        col.humidity,
204                        CONFIG.forest_hum,
205                        0.4,
206                    )) * col.tree_density
207                        * MUSH_FACT
208                        * 600.0
209                        * density_factor_by_altitude(200.0, col.alt, 1000.0), /* To control
210                                                                               * spawning based
211                                                                               * on altitude */
212                    Some((0.0, 100.0, 0.15)),
213                )
214            },
215        },
216        // Herbs and Spices
217        ScatterConfig {
218            kind: LingonBerry,
219            water_mode: Ground,
220            permit: |b| matches!(b, BlockKind::Grass),
221            f: |_, col| {
222                (
223                    close(col.temp, 0.3, 0.4).min(close(col.humidity, CONFIG.jungle_hum, 0.5))
224                        * MUSH_FACT
225                        * 2.5,
226                    None,
227                )
228            },
229        },
230        ScatterConfig {
231            kind: LeafyPlant,
232            water_mode: Ground,
233            permit: |b| matches!(b, BlockKind::Grass),
234            f: |_, col| {
235                (
236                    close(col.temp, 0.3, 0.4).min(close(col.humidity, CONFIG.jungle_hum, 0.3))
237                        * GRASS_FACT
238                        * 4.0,
239                    None,
240                )
241            },
242        },
243        ScatterConfig {
244            kind: JungleLeafyPlant,
245            water_mode: Ground,
246            permit: |b| matches!(b, BlockKind::Grass),
247            f: |_, col| {
248                (
249                    close(col.temp, 0.3, 0.4).min(close(col.humidity, CONFIG.jungle_hum, 0.4))
250                        * GRASS_FACT
251                        * 32.0,
252                    Some((0.15, 64.0, 0.2)),
253                )
254            },
255        },
256        ScatterConfig {
257            kind: Fern,
258            water_mode: Ground,
259            permit: |b| matches!(b, BlockKind::Grass),
260            f: |_, col| {
261                (
262                    close(col.temp, 0.3, 0.4).min(close(col.humidity, CONFIG.forest_hum, 0.5))
263                        * GRASS_FACT
264                        * 0.25,
265                    Some((0.0, 64.0, 0.2)),
266                )
267            },
268        },
269        ScatterConfig {
270            kind: JungleFern,
271            water_mode: Ground,
272            permit: |b| matches!(b, BlockKind::Grass),
273            f: |_, col| {
274                (
275                    close(col.temp, 0.3, 0.4).min(close(col.humidity, CONFIG.jungle_hum, 0.4))
276                        * col.tree_density
277                        * MUSH_FACT
278                        * 200.0,
279                    Some((0.0, 84.0, 0.35)),
280                )
281            },
282        },
283        ScatterConfig {
284            kind: Blueberry,
285            water_mode: Ground,
286            permit: |b| matches!(b, BlockKind::Grass),
287            f: |_, col| {
288                (
289                    close(col.temp, CONFIG.temperate_temp, 0.5).min(close(
290                        col.humidity,
291                        CONFIG.forest_hum,
292                        0.5,
293                    )) * MUSH_FACT
294                        * 0.3,
295                    None,
296                )
297            },
298        },
299        ScatterConfig {
300            kind: Pumpkin,
301            water_mode: Ground,
302            permit: |b| matches!(b, BlockKind::Grass),
303            f: if calendar.is_some_and(|calendar| calendar.is_event(CalendarEvent::Halloween)) {
304                |_, _| (0.1, Some((0.0003, 128.0, 0.1)))
305            } else {
306                |_, col| {
307                    (
308                        close(col.temp, CONFIG.temperate_temp, 0.5).min(close(
309                            col.humidity,
310                            CONFIG.forest_hum,
311                            0.5,
312                        )) * MUSH_FACT
313                            * 500.0,
314                        Some((0.0, 512.0, 0.05)),
315                    )
316                }
317            },
318        },
319        // Collectable Objects
320        // Only spawn twigs in temperate forests
321        ScatterConfig {
322            kind: Twigs,
323            water_mode: Ground,
324            permit: |b| matches!(b, BlockKind::Grass),
325            f: |_, col| {
326                (
327                    (col.tree_density * 1.25 - 0.25).powf(0.5).max(0.0) * TREE_FACT * 5.0,
328                    None,
329                )
330            },
331        },
332        // Only spawn logs in temperate forests (arbitrarily set to ~20% twig density)
333        ScatterConfig {
334            kind: Wood,
335            water_mode: Ground,
336            permit: |b| matches!(b, BlockKind::Grass),
337            f: |_, col| {
338                (
339                    (col.tree_density * 1.25 - 0.25).powf(0.5).max(0.0) * TREE_FACT,
340                    None,
341                )
342            },
343        },
344        ScatterConfig {
345            kind: Hardwood,
346            water_mode: Ground,
347            permit: |b| matches!(b, BlockKind::Grass),
348            f: |_, col| {
349                (
350                    ((close(col.temp, CONFIG.tropical_temp + 0.1, 0.3).min(close(
351                        col.humidity,
352                        CONFIG.jungle_hum,
353                        0.4,
354                    )) > 0.0) as i32) as f32
355                        * (col.tree_density * 1.25 - 0.25).powf(0.5).max(0.0)
356                        * TREE_FACT
357                        * 0.75,
358                    None,
359                )
360            },
361        },
362        // This is just a placeholder for future biomes
363        // Currently Ironwood has been included in the world\src\site\plot\giant_tree.rs
364        // ScatterConfig {
365        //     kind: Ironwood,
366        //     water_mode: Ground,
367        //     permit: |b| matches!(b, BlockKind::Wood | BlockKind::Grass),
368        //     f: |_, col| {
369        //     },
370        // },
371        ScatterConfig {
372            kind: Frostwood,
373            water_mode: Ground,
374            permit: |b| matches!(b, BlockKind::Snow | BlockKind::Ice),
375            f: |_, col| {
376                (
377                    (col.tree_density * 1.25 - 0.25).powf(0.5).max(0.0) * TREE_FACT * 0.5,
378                    None,
379                )
380            },
381        },
382        ScatterConfig {
383            kind: Stones,
384            water_mode: Ground,
385            permit: |b| {
386                matches!(
387                    b,
388                    BlockKind::Earth
389                        | BlockKind::Grass
390                        | BlockKind::Rock
391                        | BlockKind::Sand
392                        | BlockKind::Snow
393                        | BlockKind::Ice
394                )
395            },
396            f: |chunk, _| ((chunk.rockiness - 0.5).max(0.025) * 1.0e-3, None),
397        },
398        ScatterConfig {
399            kind: Copper,
400            water_mode: Ground,
401            permit: |b| {
402                matches!(
403                    b,
404                    BlockKind::Earth | BlockKind::Grass | BlockKind::Rock | BlockKind::Sand
405                )
406            },
407            f: |chunk, _| ((chunk.rockiness - 0.5).max(0.0) * 0.85e-3, None),
408        },
409        ScatterConfig {
410            kind: Tin,
411            water_mode: Ground,
412            permit: |b| {
413                matches!(
414                    b,
415                    BlockKind::Earth | BlockKind::Grass | BlockKind::Rock | BlockKind::Sand
416                )
417            },
418            f: |chunk, _| ((chunk.rockiness - 0.5).max(0.0) * 0.85e-3, None),
419        },
420        // Don't spawn Mushrooms in snowy regions
421        ScatterConfig {
422            kind: Mushroom,
423            water_mode: Ground,
424            permit: |b| matches!(b, BlockKind::Grass),
425            f: |_, col| {
426                (
427                    close(col.temp, 0.3, 0.4).min(close(col.humidity, CONFIG.forest_hum, 0.35))
428                        * MUSH_FACT,
429                    None,
430                )
431            },
432        },
433        // Grass
434        ScatterConfig {
435            kind: ShortGrass,
436            water_mode: Ground,
437            permit: |b| matches!(b, BlockKind::Grass),
438            f: |_, col| {
439                (
440                    close(col.temp, 0.2, 0.75).min(close(col.humidity, CONFIG.jungle_hum, 0.4))
441                        * GRASS_FACT
442                        * 150.0,
443                    Some((0.3, 64.0, 0.3)),
444                )
445            },
446        },
447        ScatterConfig {
448            kind: ShortGrass,
449            water_mode: Ground,
450            permit: |b| matches!(b, BlockKind::Snow),
451            f: |_, col| {
452                (
453                    close(col.temp, CONFIG.snow_temp - 0.2, 0.4).min(close(
454                        col.humidity,
455                        CONFIG.forest_hum,
456                        0.5,
457                    )) * GRASS_FACT
458                        * 50.0,
459                    Some((0.0, 48.0, 0.2)),
460                )
461            },
462        },
463        ScatterConfig {
464            kind: MediumGrass,
465            water_mode: Ground,
466            permit: |b| matches!(b, BlockKind::Grass),
467            f: |_, col| {
468                (
469                    close(col.temp, 0.2, 0.6).min(close(col.humidity, CONFIG.jungle_hum, 0.4))
470                        * GRASS_FACT
471                        * 120.0,
472                    Some((0.3, 64.0, 0.3)),
473                )
474            },
475        },
476        ScatterConfig {
477            kind: LongGrass,
478            water_mode: Ground,
479            permit: |b| matches!(b, BlockKind::Grass),
480            f: |_, col| {
481                (
482                    close(col.temp, 0.3, 0.35).min(close(col.humidity, CONFIG.jungle_hum, 0.3))
483                        * GRASS_FACT
484                        * 150.0,
485                    Some((0.1, 48.0, 0.3)),
486                )
487            },
488        },
489        ScatterConfig {
490            kind: LongGrass,
491            water_mode: Ground,
492            permit: |b| matches!(b, BlockKind::Snow),
493            f: |_, col| {
494                (
495                    close(col.temp, CONFIG.snow_temp - 0.2, 0.4).min(close(
496                        col.humidity,
497                        CONFIG.forest_hum,
498                        0.5,
499                    )) * GRASS_FACT
500                        * 25.0,
501                    Some((0.0, 48.0, 0.2)),
502                )
503            },
504        },
505        ScatterConfig {
506            kind: JungleRedGrass,
507            water_mode: Ground,
508            permit: |b| matches!(b, BlockKind::Grass),
509            f: |_, col| {
510                (
511                    close(col.temp, 0.3, 0.4).min(close(col.humidity, CONFIG.jungle_hum, 0.4))
512                        * col.tree_density
513                        * MUSH_FACT
514                        * 350.0,
515                    Some((0.0, 128.0, 0.25)),
516                )
517            },
518        },
519        // Jungle Sprites
520        // (LongGrass, Ground, |c, col| {
521        //     (
522        //         close(col.temp, CONFIG.tropical_temp, 0.4).min(close(
523        //             col.humidity,
524        //             CONFIG.jungle_hum,
525        //             0.6,
526        //         )) * 0.08,
527        //         Some((0.0, 60.0, 5.0)),
528        //     )
529        // }),
530        /*(WheatGreen, Ground, |c, col| {
531            (
532                close(col.temp, 0.4, 0.2).min(close(col.humidity, CONFIG.forest_hum, 0.1))
533                    * MUSH_FACT
534                    * 0.001,
535                None,
536            )
537        }),*/
538        ScatterConfig {
539            kind: TaigaGrass,
540            water_mode: Ground,
541            permit: |b| matches!(b, BlockKind::Grass),
542            f: |_, col| {
543                (
544                    close(col.temp, CONFIG.snow_temp - 0.2, 0.4).min(close(
545                        col.humidity,
546                        CONFIG.forest_hum,
547                        0.5,
548                    )) * GRASS_FACT
549                        * 100.0,
550                    Some((0.0, 48.0, 0.2)),
551                )
552            },
553        },
554        ScatterConfig {
555            kind: Moonbell,
556            water_mode: Ground,
557            permit: |b| matches!(b, BlockKind::Grass),
558            f: |_, col| {
559                (
560                    close(col.temp, CONFIG.snow_temp - 0.2, 0.4).min(close(
561                        col.humidity,
562                        CONFIG.forest_hum,
563                        0.5,
564                    )) * 0.003,
565                    Some((0.0, 48.0, 0.2)),
566                )
567            },
568        },
569        // Savanna Plants
570        ScatterConfig {
571            kind: SavannaGrass,
572            water_mode: Ground,
573            permit: |b| matches!(b, BlockKind::Grass),
574            f: |_, col| {
575                (
576                    {
577                        let savanna = close(col.temp, 1.0, 0.4) * close(col.humidity, 0.2, 0.25);
578                        let desert = close(col.temp, 1.0, 0.25) * close(col.humidity, 0.0, 0.1);
579                        (savanna - desert * 5.0).max(0.0) * GRASS_FACT * 250.0
580                    },
581                    Some((0.15, 64.0, 0.2)),
582                )
583            },
584        },
585        ScatterConfig {
586            kind: TallSavannaGrass,
587            water_mode: Ground,
588            permit: |b| matches!(b, BlockKind::Grass),
589            f: |_, col| {
590                (
591                    {
592                        let savanna = close(col.temp, 1.0, 0.4) * close(col.humidity, 0.2, 0.25);
593                        let desert = close(col.temp, 1.0, 0.25) * close(col.humidity, 0.0, 0.1);
594                        (savanna - desert * 5.0).max(0.0) * GRASS_FACT * 150.0
595                    },
596                    Some((0.1, 48.0, 0.2)),
597                )
598            },
599        },
600        ScatterConfig {
601            kind: RedSavannaGrass,
602            water_mode: Ground,
603            permit: |b| matches!(b, BlockKind::Grass),
604            f: |_, col| {
605                (
606                    {
607                        let savanna = close(col.temp, 1.0, 0.4) * close(col.humidity, 0.2, 0.25);
608                        let desert = close(col.temp, 1.0, 0.25) * close(col.humidity, 0.0, 0.1);
609                        (savanna - desert * 5.0).max(0.0) * GRASS_FACT * 120.0
610                    },
611                    Some((0.15, 48.0, 0.25)),
612                )
613            },
614        },
615        ScatterConfig {
616            kind: SavannaBush,
617            water_mode: Ground,
618            permit: |b| matches!(b, BlockKind::Grass),
619            f: |_, col| {
620                (
621                    {
622                        let savanna = close(col.temp, 1.0, 0.4) * close(col.humidity, 0.2, 0.25);
623                        let desert = close(col.temp, 1.0, 0.25) * close(col.humidity, 0.0, 0.1);
624                        (savanna - desert * 5.0).max(0.0) * GRASS_FACT * 40.0
625                    },
626                    Some((0.1, 96.0, 0.15)),
627                )
628            },
629        },
630        // Desert Plants
631        ScatterConfig {
632            kind: DeadBush,
633            water_mode: Ground,
634            permit: |b| matches!(b, BlockKind::Grass | BlockKind::Snow),
635            f: |_, col| {
636                (
637                    close(col.temp, 1.0, 0.95)
638                        .max(close(col.temp, CONFIG.snow_temp, 0.95))
639                        .min(close(col.humidity, 0.0, 0.45))
640                        * MUSH_FACT
641                        * 7.5,
642                    None,
643                )
644            },
645        },
646        ScatterConfig {
647            kind: Pyrebloom,
648            water_mode: Ground,
649            permit: |b| matches!(b, BlockKind::Grass),
650            f: |_, col| {
651                (
652                    close(col.temp, CONFIG.desert_temp, 0.25).min(close(col.humidity, 0.0, 0.2))
653                        * MUSH_FACT
654                        * 0.1,
655                    None,
656                )
657            },
658        },
659        ScatterConfig {
660            kind: LargeCactus,
661            water_mode: Ground,
662            permit: |b| matches!(b, BlockKind::Grass),
663            f: |_, col| {
664                (
665                    close(col.temp, CONFIG.desert_temp, 0.25).min(close(col.humidity, 0.0, 0.2))
666                        * MUSH_FACT
667                        * 1.5,
668                    None,
669                )
670            },
671        },
672        ScatterConfig {
673            kind: BarrelCactus,
674            water_mode: Ground,
675            permit: |b| matches!(b, BlockKind::Grass),
676            f: |_, col| {
677                (
678                    close(col.temp, CONFIG.desert_temp, 0.25).min(close(col.humidity, 0.0, 0.2))
679                        * MUSH_FACT
680                        * 2.0,
681                    None,
682                )
683            },
684        },
685        ScatterConfig {
686            kind: TallCactus,
687            water_mode: Ground,
688            permit: |b| matches!(b, BlockKind::Grass),
689            f: |_, col| {
690                (
691                    close(col.temp, CONFIG.desert_temp, 0.25).min(close(col.humidity, 0.0, 0.2))
692                        * MUSH_FACT
693                        * 1.5,
694                    None,
695                )
696            },
697        },
698        ScatterConfig {
699            kind: RoundCactus,
700            water_mode: Ground,
701            permit: |b| matches!(b, BlockKind::Grass),
702            f: |_, col| {
703                (
704                    close(col.temp, CONFIG.desert_temp, 0.25).min(close(col.humidity, 0.0, 0.2))
705                        * MUSH_FACT
706                        * 2.0,
707                    None,
708                )
709            },
710        },
711        ScatterConfig {
712            kind: ShortCactus,
713            water_mode: Ground,
714            permit: |b| matches!(b, BlockKind::Grass),
715            f: |_, col| {
716                (
717                    close(col.temp, CONFIG.desert_temp, 0.25).min(close(col.humidity, 0.0, 0.2))
718                        * MUSH_FACT
719                        * 2.0,
720                    None,
721                )
722            },
723        },
724        ScatterConfig {
725            kind: MedFlatCactus,
726            water_mode: Ground,
727            permit: |b| matches!(b, BlockKind::Grass),
728            f: |_, col| {
729                (
730                    close(col.temp, CONFIG.desert_temp, 0.25).min(close(col.humidity, 0.0, 0.2))
731                        * MUSH_FACT
732                        * 2.0,
733                    None,
734                )
735            },
736        },
737        ScatterConfig {
738            kind: ShortFlatCactus,
739            water_mode: Ground,
740            permit: |b| matches!(b, BlockKind::Grass),
741            f: |_, col| {
742                (
743                    close(col.temp, CONFIG.desert_temp, 0.25).min(close(col.humidity, 0.0, 0.2))
744                        * MUSH_FACT
745                        * 2.0,
746                    None,
747                )
748            },
749        },
750        // Underwater chests
751        ScatterConfig {
752            kind: ChestBuried,
753            water_mode: Underwater,
754            permit: |b| matches!(b, BlockKind::Earth | BlockKind::Sand),
755            f: |_, col| {
756                (
757                    MUSH_FACT
758                        * 1.0e-6
759                        * if col.alt < col.water_level - DEPTH_WATER_NORM + 30.0 {
760                            1.0
761                        } else {
762                            0.0
763                        },
764                    None,
765                )
766            },
767        },
768        // Underwater mud piles
769        ScatterConfig {
770            kind: Mud,
771            water_mode: Underwater,
772            permit: |b| matches!(b, BlockKind::Earth | BlockKind::Sand),
773            f: |_, col| {
774                (
775                    MUSH_FACT
776                        * 1.0e-3
777                        * if col.alt < col.water_level - DEPTH_WATER_NORM {
778                            1.0
779                        } else {
780                            0.0
781                        },
782                    None,
783                )
784            },
785        },
786        // Underwater grass
787        ScatterConfig {
788            kind: GrassBlue,
789            water_mode: Underwater,
790            permit: |b| matches!(b, BlockKind::Grass | BlockKind::Sand),
791            f: |_, col| {
792                (
793                    MUSH_FACT
794                        * 250.0
795                        * if col.alt < col.water_level - DEPTH_WATER_NORM {
796                            1.0
797                        } else {
798                            0.0
799                        },
800                    Some((0.0, 100.0, 0.15)),
801                )
802            },
803        },
804        // seagrass
805        ScatterConfig {
806            kind: Seagrass,
807            water_mode: Underwater,
808            permit: |b| matches!(b, BlockKind::Grass | BlockKind::Sand),
809            f: |_, col| {
810                (
811                    close(col.temp, CONFIG.temperate_temp, 0.8)
812                        * MUSH_FACT
813                        * 300.0
814                        * if col.water_level <= CONFIG.sea_level
815                            && col.alt < col.water_level - DEPTH_WATER_NORM + 18.0
816                        {
817                            1.0
818                        } else {
819                            0.0
820                        },
821                    Some((0.0, 150.0, 0.3)),
822                )
823            },
824        },
825        // seagrass, coastal patches
826        ScatterConfig {
827            kind: Seagrass,
828            water_mode: Underwater,
829            permit: |b| matches!(b, BlockKind::Grass | BlockKind::Sand),
830            f: |_, col| {
831                (
832                    MUSH_FACT
833                        * 600.0
834                        * if col.water_level <= CONFIG.sea_level
835                            && (col.water_level - col.alt) < 3.0
836                        {
837                            1.0
838                        } else {
839                            0.0
840                        },
841                    Some((0.0, 150.0, 0.4)),
842                )
843            },
844        },
845        // scattered seaweed (temperate species)
846        ScatterConfig {
847            kind: SeaweedTemperate,
848            water_mode: Underwater,
849            permit: |b| matches!(b, BlockKind::Grass | BlockKind::Sand),
850            f: |_, col| {
851                (
852                    close(col.temp, CONFIG.temperate_temp, 0.8)
853                        * MUSH_FACT
854                        * 50.0
855                        * if col.water_level <= CONFIG.sea_level
856                            && col.alt < col.water_level - DEPTH_WATER_NORM + 11.0
857                        {
858                            1.0
859                        } else {
860                            0.0
861                        },
862                    Some((0.0, 500.0, 0.75)),
863                )
864            },
865        },
866        // scattered seaweed (tropical species)
867        ScatterConfig {
868            kind: SeaweedTropical,
869            water_mode: Underwater,
870            permit: |b| matches!(b, BlockKind::Grass | BlockKind::Sand),
871            f: |_, col| {
872                (
873                    close(col.temp, 1.0, 0.95)
874                        * MUSH_FACT
875                        * 50.0
876                        * if col.water_level <= CONFIG.sea_level
877                            && col.alt < col.water_level - DEPTH_WATER_NORM + 11.0
878                        {
879                            1.0
880                        } else {
881                            0.0
882                        },
883                    Some((0.0, 500.0, 0.75)),
884                )
885            },
886        },
887        // Caulerpa lentillifera algae patch
888        ScatterConfig {
889            kind: SeaGrapes,
890            water_mode: Underwater,
891            permit: |b| matches!(b, BlockKind::Earth | BlockKind::Sand),
892            f: |_, col| {
893                (
894                    MUSH_FACT
895                        * 250.0
896                        * if col.water_level <= CONFIG.sea_level
897                            && col.alt < col.water_level - DEPTH_WATER_NORM + 10.0
898                        {
899                            1.0
900                        } else {
901                            0.0
902                        },
903                    Some((0.0, 100.0, 0.15)),
904                )
905            },
906        },
907        // Caulerpa prolifera algae patch
908        ScatterConfig {
909            kind: WavyAlgae,
910            water_mode: Underwater,
911            permit: |b| matches!(b, BlockKind::Earth | BlockKind::Sand),
912            f: |_, col| {
913                (
914                    MUSH_FACT
915                        * 250.0
916                        * if col.water_level <= CONFIG.sea_level
917                            && col.alt < col.water_level - DEPTH_WATER_NORM + 10.0
918                        {
919                            1.0
920                        } else {
921                            0.0
922                        },
923                    Some((0.0, 100.0, 0.15)),
924                )
925            },
926        },
927        // Mermaids' fan algae patch
928        ScatterConfig {
929            kind: MermaidsFan,
930            water_mode: Underwater,
931            permit: |b| matches!(b, BlockKind::Earth | BlockKind::Sand),
932            f: |_, col| {
933                (
934                    close(col.temp, 1.0, 0.95)
935                        * MUSH_FACT
936                        * 500.0
937                        * if col.water_level <= CONFIG.sea_level
938                            && col.alt < col.water_level - DEPTH_WATER_NORM + 10.0
939                        {
940                            1.0
941                        } else {
942                            0.0
943                        },
944                    Some((0.0, 50.0, 0.10)),
945                )
946            },
947        },
948        // Sea anemones
949        ScatterConfig {
950            kind: SeaAnemone,
951            water_mode: Underwater,
952            permit: |b| matches!(b, BlockKind::Earth | BlockKind::Sand),
953            f: |_, col| {
954                (
955                    close(col.temp, CONFIG.temperate_temp, 0.8)
956                        * MUSH_FACT
957                        * 125.0
958                        * if col.water_level <= CONFIG.sea_level
959                            && col.alt < col.water_level - DEPTH_WATER_NORM - 9.0
960                        {
961                            1.0
962                        } else {
963                            0.0
964                        },
965                    Some((0.0, 100.0, 0.3)),
966                )
967            },
968        },
969        // Giant Kelp
970        ScatterConfig {
971            kind: GiantKelp,
972            water_mode: Underwater,
973            permit: |b| matches!(b, BlockKind::Earth | BlockKind::Sand),
974            f: |_, col| {
975                (
976                    close(col.temp, CONFIG.temperate_temp, 0.8)
977                        * MUSH_FACT
978                        * 220.0
979                        * if col.water_level <= CONFIG.sea_level
980                            && col.alt < col.water_level - DEPTH_WATER_NORM - 9.0
981                        {
982                            1.0
983                        } else {
984                            0.0
985                        },
986                    Some((0.0, 200.0, 0.4)),
987                )
988            },
989        },
990        // Bull Kelp
991        ScatterConfig {
992            kind: BullKelp,
993            water_mode: Underwater,
994            permit: |b| matches!(b, BlockKind::Earth | BlockKind::Sand),
995            f: |_, col| {
996                (
997                    close(col.temp, CONFIG.temperate_temp, 0.7)
998                        * MUSH_FACT
999                        * 300.0
1000                        * if col.water_level <= CONFIG.sea_level
1001                            && col.alt < col.water_level - DEPTH_WATER_NORM + 3.0
1002                        {
1003                            1.0
1004                        } else {
1005                            0.0
1006                        },
1007                    Some((0.0, 75.0, 0.3)),
1008                )
1009            },
1010        },
1011        // Stony Corals
1012        ScatterConfig {
1013            kind: StonyCoral,
1014            water_mode: Underwater,
1015            permit: |b| matches!(b, BlockKind::Earth | BlockKind::Sand),
1016            f: |_, col| {
1017                (
1018                    close(col.temp, 1.0, 0.9)
1019                        * MUSH_FACT
1020                        * 160.0
1021                        * if col.water_level <= CONFIG.sea_level
1022                            && col.alt < col.water_level - DEPTH_WATER_NORM + 10.0
1023                        {
1024                            1.0
1025                        } else {
1026                            0.0
1027                        },
1028                    Some((0.0, 120.0, 0.4)),
1029                )
1030            },
1031        },
1032        // Soft Corals
1033        ScatterConfig {
1034            kind: SoftCoral,
1035            water_mode: Underwater,
1036            permit: |b| matches!(b, BlockKind::Earth | BlockKind::Sand),
1037            f: |_, col| {
1038                (
1039                    close(col.temp, 1.0, 0.9)
1040                        * MUSH_FACT
1041                        * 120.0
1042                        * if col.water_level <= CONFIG.sea_level
1043                            && col.alt < col.water_level - DEPTH_WATER_NORM + 10.0
1044                        {
1045                            1.0
1046                        } else {
1047                            0.0
1048                        },
1049                    Some((0.0, 120.0, 0.4)),
1050                )
1051            },
1052        },
1053        // Seashells
1054        ScatterConfig {
1055            kind: Seashells,
1056            water_mode: Underwater,
1057            permit: |b| matches!(b, BlockKind::Earth | BlockKind::Sand),
1058            f: |c, col| {
1059                (
1060                    (c.rockiness - 0.5).max(0.0)
1061                        * 1.0e-3
1062                        * if col.water_level <= CONFIG.sea_level
1063                            && col.alt < col.water_level - DEPTH_WATER_NORM + 20.0
1064                        {
1065                            1.0
1066                        } else {
1067                            0.0
1068                        },
1069                    None,
1070                )
1071            },
1072        },
1073        ScatterConfig {
1074            kind: Stones,
1075            water_mode: Underwater,
1076            permit: |b| matches!(b, BlockKind::Earth | BlockKind::Sand),
1077            f: |c, col| {
1078                (
1079                    (c.rockiness - 0.5).max(0.0)
1080                        * 1.0e-3
1081                        * if col.alt < col.water_level - DEPTH_WATER_NORM {
1082                            1.0
1083                        } else {
1084                            0.0
1085                        },
1086                    None,
1087                )
1088            },
1089        },
1090        //River-related scatter
1091        ScatterConfig {
1092            kind: LillyPads,
1093            water_mode: Floating,
1094            permit: |_| true,
1095            f: |_, col| {
1096                (
1097                    close(col.temp, 0.2, 0.6).min(close(col.humidity, CONFIG.jungle_hum, 0.4))
1098                        * GRASS_FACT
1099                        * 100.0
1100                        * ((col.alt - CONFIG.sea_level) / 12.0).clamped(0.0, 1.0)
1101                        * col
1102                            .water_dist
1103                            .map_or(0.0, |d| 1.0 / (1.0 + (d.abs() * 0.4).powi(2))),
1104                    Some((0.0, 128.0, 0.35)),
1105                )
1106            },
1107        },
1108        ScatterConfig {
1109            kind: Reed,
1110            water_mode: Underwater,
1111            permit: |b| matches!(b, BlockKind::Grass),
1112            f: |_, col| {
1113                (
1114                    close(col.temp, 0.2, 0.6).min(close(col.humidity, CONFIG.jungle_hum, 0.4))
1115                        * GRASS_FACT
1116                        * 100.0
1117                        * ((col.alt - CONFIG.sea_level) / 12.0).clamped(0.0, 1.0)
1118                        * col
1119                            .water_dist
1120                            .map_or(0.0, |d| 1.0 / (1.0 + (d.abs() * 0.40).powi(2))),
1121                    Some((0.2, 128.0, 0.5)),
1122                )
1123            },
1124        },
1125        ScatterConfig {
1126            kind: Reed,
1127            water_mode: Ground,
1128            permit: |b| matches!(b, BlockKind::Grass),
1129            f: |_, col| {
1130                (
1131                    close(col.humidity, CONFIG.jungle_hum, 0.9)
1132                        * col
1133                            .water_dist
1134                            .map(|wd| Lerp::lerp(0.2, 0.0, (wd / 8.0).clamped(0.0, 1.0)))
1135                            .unwrap_or(0.0)
1136                        * ((col.alt - CONFIG.sea_level) / 12.0).clamped(0.0, 1.0),
1137                    Some((0.2, 128.0, 0.5)),
1138                )
1139            },
1140        },
1141        ScatterConfig {
1142            kind: Bamboo,
1143            water_mode: Ground,
1144            permit: |b| matches!(b, BlockKind::Grass),
1145            f: |_, col| {
1146                (
1147                    0.014
1148                        * close(col.humidity, CONFIG.jungle_hum, 0.9)
1149                        * col
1150                            .water_dist
1151                            .map(|wd| Lerp::lerp(0.2, 0.0, (wd / 8.0).clamped(0.0, 1.0)))
1152                            .unwrap_or(0.0)
1153                        * ((col.alt - CONFIG.sea_level) / 12.0).clamped(0.0, 1.0),
1154                    Some((0.2, 128.0, 0.5)),
1155                )
1156            },
1157        },
1158    ];
1159
1160    canvas.foreach_col(|canvas, wpos2d, col| {
1161        let underwater = col.water_level.floor() > col.alt;
1162
1163        let kind = scatter.iter().enumerate().find_map(
1164            |(
1165                i,
1166                ScatterConfig {
1167                    kind,
1168                    water_mode,
1169                    permit,
1170                    f,
1171                },
1172            )| {
1173                let block_kind = canvas
1174                    .get(Vec3::new(wpos2d.x, wpos2d.y, col.alt as i32))
1175                    .kind();
1176                if !permit(block_kind) {
1177                    return None;
1178                }
1179                let snow_covered = matches!(block_kind, BlockKind::Snow | BlockKind::Ice);
1180                let (density, patch) = f(canvas.chunk(), col);
1181                let density = patch
1182                    .map(|(base_density_prop, wavelen, threshold)| {
1183                        if canvas
1184                            .index()
1185                            .noise
1186                            .scatter_nz
1187                            .get(
1188                                wpos2d
1189                                    .map(|e| e as f64 / wavelen as f64 + i as f64 * 43.0)
1190                                    .into_array(),
1191                            )
1192                            .abs()
1193                            > 1.0 - threshold as f64
1194                        {
1195                            density
1196                        } else {
1197                            density * base_density_prop
1198                        }
1199                    })
1200                    .unwrap_or(density);
1201                if density > 0.0
1202                    // Now deterministic, chunk resources are tracked by rtsim
1203                    && /*rng.random::<f32>() < density*/ RandomField::new(i as u32).chance(Vec3::new(wpos2d.x, wpos2d.y, 0), density)
1204                    && matches!(&water_mode, Underwater | Floating) == underwater
1205                {
1206                    Some((*kind, snow_covered, water_mode))
1207                } else {
1208                    None
1209                }
1210            },
1211        );
1212
1213        if let Some((kind, snow_covered, water_mode)) = kind {
1214            let (alt, is_under): (_, fn(Block) -> bool) = match water_mode {
1215                Ground | Underwater => (col.alt as i32, |block| block.is_solid()),
1216                Floating => (col.water_level as i32, |block| !block.is_air()),
1217            };
1218
1219            // Find the intersection between ground and air, if there is one near the
1220            // Ground
1221            if let Some(solid_end) = (-4..8)
1222                .find(|z| is_under(canvas.get(Vec3::new(wpos2d.x, wpos2d.y, alt + z))))
1223                .and_then(|solid_start| {
1224                    (1..8)
1225                        .map(|z| solid_start + z)
1226                        .find(|z| !is_under(canvas.get(Vec3::new(wpos2d.x, wpos2d.y, alt + z))))
1227                })
1228            {
1229                canvas.map_resource(Vec3::new(wpos2d.x, wpos2d.y, alt + solid_end), |block| {
1230                    let mut block = block.with_sprite(kind);
1231                    if block.sprite_category().is_some_and(|category| category.has_attr::<SnowCovered>()) {
1232                        block = block.with_attr(SnowCovered(snow_covered)).expect("`Category::has_attr` should have ensured setting the attribute will succeed");
1233                    }
1234
1235                    block
1236                });
1237            }
1238        }
1239    });
1240}