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\site2\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.3).max(close(
640                            col.humidity,
641                            CONFIG.forest_hum,
642                            0.3,
643                        )))
644                        * MUSH_FACT
645                        * 7.5,
646                    None,
647                )
648            },
649        },
650        ScatterConfig {
651            kind: Pyrebloom,
652            water_mode: Ground,
653            permit: |b| matches!(b, BlockKind::Grass),
654            f: |_, col| {
655                (
656                    close(col.temp, CONFIG.desert_temp, 0.25).min(close(col.humidity, 0.0, 0.2))
657                        * MUSH_FACT
658                        * 0.1,
659                    None,
660                )
661            },
662        },
663        ScatterConfig {
664            kind: LargeCactus,
665            water_mode: Ground,
666            permit: |b| matches!(b, BlockKind::Grass),
667            f: |_, col| {
668                (
669                    close(col.temp, CONFIG.desert_temp, 0.25).min(close(col.humidity, 0.0, 0.2))
670                        * MUSH_FACT
671                        * 1.5,
672                    None,
673                )
674            },
675        },
676        ScatterConfig {
677            kind: BarrelCactus,
678            water_mode: Ground,
679            permit: |b| matches!(b, BlockKind::Grass),
680            f: |_, col| {
681                (
682                    close(col.temp, CONFIG.desert_temp, 0.25).min(close(col.humidity, 0.0, 0.2))
683                        * MUSH_FACT
684                        * 2.0,
685                    None,
686                )
687            },
688        },
689        ScatterConfig {
690            kind: TallCactus,
691            water_mode: Ground,
692            permit: |b| matches!(b, BlockKind::Grass),
693            f: |_, col| {
694                (
695                    close(col.temp, CONFIG.desert_temp, 0.25).min(close(col.humidity, 0.0, 0.2))
696                        * MUSH_FACT
697                        * 1.5,
698                    None,
699                )
700            },
701        },
702        ScatterConfig {
703            kind: RoundCactus,
704            water_mode: Ground,
705            permit: |b| matches!(b, BlockKind::Grass),
706            f: |_, col| {
707                (
708                    close(col.temp, CONFIG.desert_temp, 0.25).min(close(col.humidity, 0.0, 0.2))
709                        * MUSH_FACT
710                        * 2.0,
711                    None,
712                )
713            },
714        },
715        ScatterConfig {
716            kind: ShortCactus,
717            water_mode: Ground,
718            permit: |b| matches!(b, BlockKind::Grass),
719            f: |_, col| {
720                (
721                    close(col.temp, CONFIG.desert_temp, 0.25).min(close(col.humidity, 0.0, 0.2))
722                        * MUSH_FACT
723                        * 2.0,
724                    None,
725                )
726            },
727        },
728        ScatterConfig {
729            kind: MedFlatCactus,
730            water_mode: Ground,
731            permit: |b| matches!(b, BlockKind::Grass),
732            f: |_, col| {
733                (
734                    close(col.temp, CONFIG.desert_temp, 0.25).min(close(col.humidity, 0.0, 0.2))
735                        * MUSH_FACT
736                        * 2.0,
737                    None,
738                )
739            },
740        },
741        ScatterConfig {
742            kind: ShortFlatCactus,
743            water_mode: Ground,
744            permit: |b| matches!(b, BlockKind::Grass),
745            f: |_, col| {
746                (
747                    close(col.temp, CONFIG.desert_temp, 0.25).min(close(col.humidity, 0.0, 0.2))
748                        * MUSH_FACT
749                        * 2.0,
750                    None,
751                )
752            },
753        },
754        // Underwater chests
755        ScatterConfig {
756            kind: ChestBuried,
757            water_mode: Underwater,
758            permit: |b| matches!(b, BlockKind::Earth | BlockKind::Sand),
759            f: |_, col| {
760                (
761                    MUSH_FACT
762                        * 1.0e-6
763                        * if col.alt < col.water_level - DEPTH_WATER_NORM + 30.0 {
764                            1.0
765                        } else {
766                            0.0
767                        },
768                    None,
769                )
770            },
771        },
772        // Underwater mud piles
773        ScatterConfig {
774            kind: Mud,
775            water_mode: Underwater,
776            permit: |b| matches!(b, BlockKind::Earth | BlockKind::Sand),
777            f: |_, col| {
778                (
779                    MUSH_FACT
780                        * 1.0e-3
781                        * if col.alt < col.water_level - DEPTH_WATER_NORM {
782                            1.0
783                        } else {
784                            0.0
785                        },
786                    None,
787                )
788            },
789        },
790        // Underwater grass
791        ScatterConfig {
792            kind: GrassBlue,
793            water_mode: Underwater,
794            permit: |b| matches!(b, BlockKind::Grass | BlockKind::Sand),
795            f: |_, col| {
796                (
797                    MUSH_FACT
798                        * 250.0
799                        * if col.alt < col.water_level - DEPTH_WATER_NORM {
800                            1.0
801                        } else {
802                            0.0
803                        },
804                    Some((0.0, 100.0, 0.15)),
805                )
806            },
807        },
808        // seagrass
809        ScatterConfig {
810            kind: Seagrass,
811            water_mode: Underwater,
812            permit: |b| matches!(b, BlockKind::Grass | BlockKind::Sand),
813            f: |_, col| {
814                (
815                    close(col.temp, CONFIG.temperate_temp, 0.8)
816                        * MUSH_FACT
817                        * 300.0
818                        * if col.water_level <= CONFIG.sea_level
819                            && col.alt < col.water_level - DEPTH_WATER_NORM + 18.0
820                        {
821                            1.0
822                        } else {
823                            0.0
824                        },
825                    Some((0.0, 150.0, 0.3)),
826                )
827            },
828        },
829        // seagrass, coastal patches
830        ScatterConfig {
831            kind: Seagrass,
832            water_mode: Underwater,
833            permit: |b| matches!(b, BlockKind::Grass | BlockKind::Sand),
834            f: |_, col| {
835                (
836                    MUSH_FACT
837                        * 600.0
838                        * if col.water_level <= CONFIG.sea_level
839                            && (col.water_level - col.alt) < 3.0
840                        {
841                            1.0
842                        } else {
843                            0.0
844                        },
845                    Some((0.0, 150.0, 0.4)),
846                )
847            },
848        },
849        // scattered seaweed (temperate species)
850        ScatterConfig {
851            kind: SeaweedTemperate,
852            water_mode: Underwater,
853            permit: |b| matches!(b, BlockKind::Grass | BlockKind::Sand),
854            f: |_, col| {
855                (
856                    close(col.temp, CONFIG.temperate_temp, 0.8)
857                        * MUSH_FACT
858                        * 50.0
859                        * if col.water_level <= CONFIG.sea_level
860                            && col.alt < col.water_level - DEPTH_WATER_NORM + 11.0
861                        {
862                            1.0
863                        } else {
864                            0.0
865                        },
866                    Some((0.0, 500.0, 0.75)),
867                )
868            },
869        },
870        // scattered seaweed (tropical species)
871        ScatterConfig {
872            kind: SeaweedTropical,
873            water_mode: Underwater,
874            permit: |b| matches!(b, BlockKind::Grass | BlockKind::Sand),
875            f: |_, col| {
876                (
877                    close(col.temp, 1.0, 0.95)
878                        * MUSH_FACT
879                        * 50.0
880                        * if col.water_level <= CONFIG.sea_level
881                            && col.alt < col.water_level - DEPTH_WATER_NORM + 11.0
882                        {
883                            1.0
884                        } else {
885                            0.0
886                        },
887                    Some((0.0, 500.0, 0.75)),
888                )
889            },
890        },
891        // Caulerpa lentillifera algae patch
892        ScatterConfig {
893            kind: SeaGrapes,
894            water_mode: Underwater,
895            permit: |b| matches!(b, BlockKind::Earth | BlockKind::Sand),
896            f: |_, col| {
897                (
898                    MUSH_FACT
899                        * 250.0
900                        * if col.water_level <= CONFIG.sea_level
901                            && col.alt < col.water_level - DEPTH_WATER_NORM + 10.0
902                        {
903                            1.0
904                        } else {
905                            0.0
906                        },
907                    Some((0.0, 100.0, 0.15)),
908                )
909            },
910        },
911        // Caulerpa prolifera algae patch
912        ScatterConfig {
913            kind: WavyAlgae,
914            water_mode: Underwater,
915            permit: |b| matches!(b, BlockKind::Earth | BlockKind::Sand),
916            f: |_, col| {
917                (
918                    MUSH_FACT
919                        * 250.0
920                        * if col.water_level <= CONFIG.sea_level
921                            && col.alt < col.water_level - DEPTH_WATER_NORM + 10.0
922                        {
923                            1.0
924                        } else {
925                            0.0
926                        },
927                    Some((0.0, 100.0, 0.15)),
928                )
929            },
930        },
931        // Mermaids' fan algae patch
932        ScatterConfig {
933            kind: MermaidsFan,
934            water_mode: Underwater,
935            permit: |b| matches!(b, BlockKind::Earth | BlockKind::Sand),
936            f: |_, col| {
937                (
938                    close(col.temp, 1.0, 0.95)
939                        * MUSH_FACT
940                        * 500.0
941                        * if col.water_level <= CONFIG.sea_level
942                            && col.alt < col.water_level - DEPTH_WATER_NORM + 10.0
943                        {
944                            1.0
945                        } else {
946                            0.0
947                        },
948                    Some((0.0, 50.0, 0.10)),
949                )
950            },
951        },
952        // Sea anemones
953        ScatterConfig {
954            kind: SeaAnemone,
955            water_mode: Underwater,
956            permit: |b| matches!(b, BlockKind::Earth | BlockKind::Sand),
957            f: |_, col| {
958                (
959                    close(col.temp, CONFIG.temperate_temp, 0.8)
960                        * MUSH_FACT
961                        * 125.0
962                        * if col.water_level <= CONFIG.sea_level
963                            && col.alt < col.water_level - DEPTH_WATER_NORM - 9.0
964                        {
965                            1.0
966                        } else {
967                            0.0
968                        },
969                    Some((0.0, 100.0, 0.3)),
970                )
971            },
972        },
973        // Giant Kelp
974        ScatterConfig {
975            kind: GiantKelp,
976            water_mode: Underwater,
977            permit: |b| matches!(b, BlockKind::Earth | BlockKind::Sand),
978            f: |_, col| {
979                (
980                    close(col.temp, CONFIG.temperate_temp, 0.8)
981                        * MUSH_FACT
982                        * 220.0
983                        * if col.water_level <= CONFIG.sea_level
984                            && col.alt < col.water_level - DEPTH_WATER_NORM - 9.0
985                        {
986                            1.0
987                        } else {
988                            0.0
989                        },
990                    Some((0.0, 200.0, 0.4)),
991                )
992            },
993        },
994        // Bull Kelp
995        ScatterConfig {
996            kind: BullKelp,
997            water_mode: Underwater,
998            permit: |b| matches!(b, BlockKind::Earth | BlockKind::Sand),
999            f: |_, col| {
1000                (
1001                    close(col.temp, CONFIG.temperate_temp, 0.7)
1002                        * MUSH_FACT
1003                        * 300.0
1004                        * if col.water_level <= CONFIG.sea_level
1005                            && col.alt < col.water_level - DEPTH_WATER_NORM + 3.0
1006                        {
1007                            1.0
1008                        } else {
1009                            0.0
1010                        },
1011                    Some((0.0, 75.0, 0.3)),
1012                )
1013            },
1014        },
1015        // Stony Corals
1016        ScatterConfig {
1017            kind: StonyCoral,
1018            water_mode: Underwater,
1019            permit: |b| matches!(b, BlockKind::Earth | BlockKind::Sand),
1020            f: |_, col| {
1021                (
1022                    close(col.temp, 1.0, 0.9)
1023                        * MUSH_FACT
1024                        * 160.0
1025                        * if col.water_level <= CONFIG.sea_level
1026                            && col.alt < col.water_level - DEPTH_WATER_NORM + 10.0
1027                        {
1028                            1.0
1029                        } else {
1030                            0.0
1031                        },
1032                    Some((0.0, 120.0, 0.4)),
1033                )
1034            },
1035        },
1036        // Soft Corals
1037        ScatterConfig {
1038            kind: SoftCoral,
1039            water_mode: Underwater,
1040            permit: |b| matches!(b, BlockKind::Earth | BlockKind::Sand),
1041            f: |_, col| {
1042                (
1043                    close(col.temp, 1.0, 0.9)
1044                        * MUSH_FACT
1045                        * 120.0
1046                        * if col.water_level <= CONFIG.sea_level
1047                            && col.alt < col.water_level - DEPTH_WATER_NORM + 10.0
1048                        {
1049                            1.0
1050                        } else {
1051                            0.0
1052                        },
1053                    Some((0.0, 120.0, 0.4)),
1054                )
1055            },
1056        },
1057        // Seashells
1058        ScatterConfig {
1059            kind: Seashells,
1060            water_mode: Underwater,
1061            permit: |b| matches!(b, BlockKind::Earth | BlockKind::Sand),
1062            f: |c, col| {
1063                (
1064                    (c.rockiness - 0.5).max(0.0)
1065                        * 1.0e-3
1066                        * if col.water_level <= CONFIG.sea_level
1067                            && col.alt < col.water_level - DEPTH_WATER_NORM + 20.0
1068                        {
1069                            1.0
1070                        } else {
1071                            0.0
1072                        },
1073                    None,
1074                )
1075            },
1076        },
1077        ScatterConfig {
1078            kind: Stones,
1079            water_mode: Underwater,
1080            permit: |b| matches!(b, BlockKind::Earth | BlockKind::Sand),
1081            f: |c, col| {
1082                (
1083                    (c.rockiness - 0.5).max(0.0)
1084                        * 1.0e-3
1085                        * if col.alt < col.water_level - DEPTH_WATER_NORM {
1086                            1.0
1087                        } else {
1088                            0.0
1089                        },
1090                    None,
1091                )
1092            },
1093        },
1094        //River-related scatter
1095        ScatterConfig {
1096            kind: LillyPads,
1097            water_mode: Floating,
1098            permit: |_| true,
1099            f: |_, col| {
1100                (
1101                    close(col.temp, 0.2, 0.6).min(close(col.humidity, CONFIG.jungle_hum, 0.4))
1102                        * GRASS_FACT
1103                        * 100.0
1104                        * ((col.alt - CONFIG.sea_level) / 12.0).clamped(0.0, 1.0)
1105                        * col
1106                            .water_dist
1107                            .map_or(0.0, |d| 1.0 / (1.0 + (d.abs() * 0.4).powi(2))),
1108                    Some((0.0, 128.0, 0.35)),
1109                )
1110            },
1111        },
1112        ScatterConfig {
1113            kind: Reed,
1114            water_mode: Underwater,
1115            permit: |b| matches!(b, BlockKind::Grass),
1116            f: |_, col| {
1117                (
1118                    close(col.temp, 0.2, 0.6).min(close(col.humidity, CONFIG.jungle_hum, 0.4))
1119                        * GRASS_FACT
1120                        * 100.0
1121                        * ((col.alt - CONFIG.sea_level) / 12.0).clamped(0.0, 1.0)
1122                        * col
1123                            .water_dist
1124                            .map_or(0.0, |d| 1.0 / (1.0 + (d.abs() * 0.40).powi(2))),
1125                    Some((0.2, 128.0, 0.5)),
1126                )
1127            },
1128        },
1129        ScatterConfig {
1130            kind: Reed,
1131            water_mode: Ground,
1132            permit: |b| matches!(b, BlockKind::Grass),
1133            f: |_, col| {
1134                (
1135                    close(col.humidity, CONFIG.jungle_hum, 0.9)
1136                        * col
1137                            .water_dist
1138                            .map(|wd| Lerp::lerp(0.2, 0.0, (wd / 8.0).clamped(0.0, 1.0)))
1139                            .unwrap_or(0.0)
1140                        * ((col.alt - CONFIG.sea_level) / 12.0).clamped(0.0, 1.0),
1141                    Some((0.2, 128.0, 0.5)),
1142                )
1143            },
1144        },
1145        ScatterConfig {
1146            kind: Bamboo,
1147            water_mode: Ground,
1148            permit: |b| matches!(b, BlockKind::Grass),
1149            f: |_, col| {
1150                (
1151                    0.014
1152                        * close(col.humidity, CONFIG.jungle_hum, 0.9)
1153                        * col
1154                            .water_dist
1155                            .map(|wd| Lerp::lerp(0.2, 0.0, (wd / 8.0).clamped(0.0, 1.0)))
1156                            .unwrap_or(0.0)
1157                        * ((col.alt - CONFIG.sea_level) / 12.0).clamped(0.0, 1.0),
1158                    Some((0.2, 128.0, 0.5)),
1159                )
1160            },
1161        },
1162    ];
1163
1164    canvas.foreach_col(|canvas, wpos2d, col| {
1165        let underwater = col.water_level.floor() > col.alt;
1166
1167        let kind = scatter.iter().enumerate().find_map(
1168            |(
1169                i,
1170                ScatterConfig {
1171                    kind,
1172                    water_mode,
1173                    permit,
1174                    f,
1175                },
1176            )| {
1177                let block_kind = canvas
1178                    .get(Vec3::new(wpos2d.x, wpos2d.y, col.alt as i32))
1179                    .kind();
1180                if !permit(block_kind) {
1181                    return None;
1182                }
1183                let snow_covered = matches!(block_kind, BlockKind::Snow | BlockKind::Ice);
1184                let (density, patch) = f(canvas.chunk(), col);
1185                let density = patch
1186                    .map(|(base_density_prop, wavelen, threshold)| {
1187                        if canvas
1188                            .index()
1189                            .noise
1190                            .scatter_nz
1191                            .get(
1192                                wpos2d
1193                                    .map(|e| e as f64 / wavelen as f64 + i as f64 * 43.0)
1194                                    .into_array(),
1195                            )
1196                            .abs()
1197                            > 1.0 - threshold as f64
1198                        {
1199                            density
1200                        } else {
1201                            density * base_density_prop
1202                        }
1203                    })
1204                    .unwrap_or(density);
1205                if density > 0.0
1206                    // Now deterministic, chunk resources are tracked by rtsim
1207                    && /*rng.gen::<f32>() < density*/ RandomField::new(i as u32).chance(Vec3::new(wpos2d.x, wpos2d.y, 0), density)
1208                    && matches!(&water_mode, Underwater | Floating) == underwater
1209                {
1210                    Some((*kind, snow_covered, water_mode))
1211                } else {
1212                    None
1213                }
1214            },
1215        );
1216
1217        if let Some((kind, snow_covered, water_mode)) = kind {
1218            let (alt, is_under): (_, fn(Block) -> bool) = match water_mode {
1219                Ground | Underwater => (col.alt as i32, |block| block.is_solid()),
1220                Floating => (col.water_level as i32, |block| !block.is_air()),
1221            };
1222
1223            // Find the intersection between ground and air, if there is one near the
1224            // Ground
1225            if let Some(solid_end) = (-4..8)
1226                .find(|z| is_under(canvas.get(Vec3::new(wpos2d.x, wpos2d.y, alt + z))))
1227                .and_then(|solid_start| {
1228                    (1..8)
1229                        .map(|z| solid_start + z)
1230                        .find(|z| !is_under(canvas.get(Vec3::new(wpos2d.x, wpos2d.y, alt + z))))
1231                })
1232            {
1233                canvas.map_resource(Vec3::new(wpos2d.x, wpos2d.y, alt + solid_end), |block| {
1234                    let mut block = block.with_sprite(kind);
1235                    if block.sprite_category().is_some_and(|category| category.has_attr::<SnowCovered>()) {
1236                        block = block.with_attr(SnowCovered(snow_covered)).expect("`Category::has_attr` should have ensured setting the attribute will succeed");
1237                    }
1238
1239                    block
1240                });
1241            }
1242        }
1243    });
1244}