veloren_world/layer/
wildlife.rs

1use crate::{CONFIG, IndexRef, column::ColumnSample, sim::SimChunk, util::close};
2use common::{
3    assets::{self, AssetExt},
4    calendar::{Calendar, CalendarEvent},
5    generation::{ChunkSupplement, EntityInfo},
6    resources::TimeOfDay,
7    terrain::{BiomeKind, Block},
8    time::DayPeriod,
9    vol::{ReadVol, RectSizedVol, WriteVol},
10};
11use rand::prelude::*;
12use serde::Deserialize;
13use std::f32;
14use vek::*;
15
16type Weight = u32;
17type Min = u8;
18type Max = u8;
19
20#[derive(Clone, Debug, Deserialize)]
21pub struct SpawnEntry {
22    /// User-facing info for wiki, statistical tools, etc.
23    pub name: String,
24    pub note: String,
25    /// Rules describing what and when to spawn
26    pub rules: Vec<Pack>,
27}
28
29impl assets::Asset for SpawnEntry {
30    type Loader = assets::RonLoader;
31
32    const EXTENSION: &'static str = "ron";
33}
34
35impl SpawnEntry {
36    pub fn from(asset_specifier: &str) -> Self { Self::load_expect_cloned(asset_specifier) }
37
38    pub fn request(
39        &self,
40        requested_period: DayPeriod,
41        calendar: Option<&Calendar>,
42        is_underwater: bool,
43        is_ice: bool,
44    ) -> Option<Pack> {
45        self.rules
46            .iter()
47            .find(|pack| {
48                let time_match = pack
49                    .day_period
50                    .iter()
51                    .any(|period| *period == requested_period);
52                let calendar_match = if let Some(calendar) = calendar {
53                    pack.calendar_events
54                        .as_ref()
55                        .is_none_or(|events| events.iter().any(|event| calendar.is_event(*event)))
56                } else {
57                    false
58                };
59                let mode_match = match pack.spawn_mode {
60                    SpawnMode::Land => !is_underwater,
61                    SpawnMode::Ice => is_ice,
62                    SpawnMode::Water | SpawnMode::Underwater => is_underwater,
63                    SpawnMode::Air(_) => true,
64                };
65                time_match && calendar_match && mode_match
66            })
67            .cloned()
68    }
69}
70
71/// Dataset of animals to spawn
72///
73/// Example:
74/// ```text
75///        Pack(
76///            groups: [
77///                (3, (1, 2, "common.entity.wild.aggressive.frostfang")),
78///                (1, (1, 1, "common.entity.wild.aggressive.snow_leopard")),
79///                (1, (1, 1, "common.entity.wild.aggressive.yale")),
80///                (1, (1, 1, "common.entity.wild.aggressive.grolgar")),
81///            ],
82///            spawn_mode: Land,
83///            day_period: [Night, Morning, Noon, Evening],
84///        ),
85/// ```
86/// Groups:
87/// ```text
88///                (3, (1, 2, "common.entity.wild.aggressive.frostfang")),
89/// ```
90/// (3, ...) means that it has x3 chance to spawn (3/6 when every other has
91/// 1/6).
92///
93/// (.., (1, 2, ...)) is `1..=2` group size which means that it has
94/// chance to spawn as single mob or in pair
95///
96/// (..., (..., "common.entity.wild.aggressive.frostfang")) corresponds
97/// to `assets/common/entity/wild/aggressive/frostfang.ron` file with
98/// EntityConfig
99///
100/// Spawn mode:
101/// `spawn_mode: Land` means mobs spawn on land at the surface (i.e: cows)
102/// `spawn_mode: means mobs spawn on the surface of water ice
103/// `spawn_mode: Water` means mobs spawn *in* water at a random depth (i.e:
104/// fish) `spawn_mode: Underwater` means mobs spawn at the bottom of a body of
105/// water (i.e: crabs) `spawn_mode: Air(32)` means mobs spawn in the air above
106/// either land or water, with a maximum altitude of 32
107///
108/// Day period:
109/// `day_period: [Night, Morning, Noon, Evening]`
110/// means that mobs from this pack may be spawned in any day period without
111/// exception
112#[derive(Clone, Debug, Deserialize)]
113pub struct Pack {
114    pub groups: Vec<(Weight, (Min, Max, String))>,
115    pub spawn_mode: SpawnMode,
116    pub day_period: Vec<DayPeriod>,
117    #[serde(default)]
118    pub calendar_events: Option<Vec<CalendarEvent>>, /* None implies that the group isn't
119                                                      * limited by calendar events */
120}
121
122#[derive(Copy, Clone, Debug, Deserialize)]
123pub enum SpawnMode {
124    Land,
125    Ice,
126    Water,
127    Underwater,
128    Air(f32),
129}
130
131impl Pack {
132    pub fn generate(&self, pos: Vec3<f32>, dynamic_rng: &mut impl Rng) -> (EntityInfo, u8) {
133        let (_, (from, to, entity_asset)) = self
134            .groups
135            .choose_weighted(dynamic_rng, |(p, _group)| *p)
136            .expect("Failed to choose group");
137        let entity = EntityInfo::at(pos).with_asset_expect(entity_asset, dynamic_rng, None);
138        let group_size = dynamic_rng.gen_range(*from..=*to);
139
140        (entity, group_size)
141    }
142}
143
144pub type DensityFn = fn(&SimChunk, &ColumnSample) -> f32;
145
146pub fn spawn_manifest() -> Vec<(&'static str, DensityFn)> {
147    const BASE_DENSITY: f32 = 1.0e-5; // Base wildlife density
148    // NOTE: Order matters.
149    // Entries with more specific requirements
150    // and overall scarcity should come first, where possible.
151    vec![
152        // **Tundra**
153        // Rock animals
154        ("world.wildlife.spawn.tundra.rock", |c, col| {
155            close(c.temp, CONFIG.snow_temp, 0.15) * BASE_DENSITY * col.rock_density * 1.0
156        }),
157        // Core animals
158        ("world.wildlife.spawn.tundra.core", |c, _col| {
159            close(c.temp, CONFIG.snow_temp, 0.15) * BASE_DENSITY * 0.5
160        }),
161        // Core animals events
162        (
163            "world.wildlife.spawn.calendar.christmas.tundra.core",
164            |c, _col| close(c.temp, CONFIG.snow_temp, 0.15) * BASE_DENSITY * 0.5,
165        ),
166        (
167            "world.wildlife.spawn.calendar.halloween.tundra.core",
168            |c, _col| close(c.temp, CONFIG.snow_temp, 0.15) * BASE_DENSITY * 1.0,
169        ),
170        (
171            "world.wildlife.spawn.calendar.april_fools.tundra.core",
172            |c, _col| close(c.temp, CONFIG.snow_temp, 0.15) * BASE_DENSITY * 0.5,
173        ),
174        (
175            "world.wildlife.spawn.calendar.easter.tundra.core",
176            |c, _col| close(c.temp, CONFIG.snow_temp, 0.15) * BASE_DENSITY * 0.5,
177        ),
178        // Snowy animals
179        ("world.wildlife.spawn.tundra.snow", |c, col| {
180            close(c.temp, CONFIG.snow_temp, 0.3) * BASE_DENSITY * col.snow_cover as i32 as f32 * 1.0
181        }),
182        // Snowy animals event
183        (
184            "world.wildlife.spawn.calendar.christmas.tundra.snow",
185            |c, col| {
186                close(c.temp, CONFIG.snow_temp, 0.3)
187                    * BASE_DENSITY
188                    * col.snow_cover as i32 as f32
189                    * 1.0
190            },
191        ),
192        (
193            "world.wildlife.spawn.calendar.halloween.tundra.snow",
194            |c, col| {
195                close(c.temp, CONFIG.snow_temp, 0.3)
196                    * BASE_DENSITY
197                    * col.snow_cover as i32 as f32
198                    * 1.5
199            },
200        ),
201        (
202            "world.wildlife.spawn.calendar.april_fools.tundra.snow",
203            |c, col| {
204                close(c.temp, CONFIG.snow_temp, 0.3)
205                    * BASE_DENSITY
206                    * col.snow_cover as i32 as f32
207                    * 1.0
208            },
209        ),
210        (
211            "world.wildlife.spawn.calendar.easter.tundra.snow",
212            |c, col| {
213                close(c.temp, CONFIG.snow_temp, 0.3)
214                    * BASE_DENSITY
215                    * col.snow_cover as i32 as f32
216                    * 1.0
217            },
218        ),
219        // Forest animals
220        ("world.wildlife.spawn.tundra.forest", |c, col| {
221            close(c.temp, CONFIG.snow_temp, 0.3) * col.tree_density * BASE_DENSITY * 1.4
222        }),
223        // River wildlife
224        ("world.wildlife.spawn.tundra.river", |c, col| {
225            close(col.temp, CONFIG.snow_temp, 0.3)
226                * if col.water_dist.map(|d| d < 1.0).unwrap_or(false)
227                    && !matches!(col.chunk.get_biome(), BiomeKind::Ocean)
228                    && c.alt > CONFIG.sea_level + 20.0
229                {
230                    0.001
231                } else {
232                    0.0
233                }
234        }),
235        // Forest animals event
236        (
237            "world.wildlife.spawn.calendar.christmas.tundra.forest",
238            |c, col| close(c.temp, CONFIG.snow_temp, 0.3) * col.tree_density * BASE_DENSITY * 1.4,
239        ),
240        (
241            "world.wildlife.spawn.calendar.halloween.tundra.forest",
242            |c, col| close(c.temp, CONFIG.snow_temp, 0.3) * col.tree_density * BASE_DENSITY * 2.0,
243        ),
244        (
245            "world.wildlife.spawn.calendar.april_fools.tundra.forest",
246            |c, col| close(c.temp, CONFIG.snow_temp, 0.3) * col.tree_density * BASE_DENSITY * 1.4,
247        ),
248        (
249            "world.wildlife.spawn.calendar.easter.tundra.forest",
250            |c, col| close(c.temp, CONFIG.snow_temp, 0.3) * col.tree_density * BASE_DENSITY * 1.4,
251        ),
252        // **Taiga**
253        // Forest core animals
254        ("world.wildlife.spawn.taiga.core_forest", |c, col| {
255            close(c.temp, CONFIG.snow_temp + 0.2, 0.2) * col.tree_density * BASE_DENSITY * 0.4
256        }),
257        // Forest core animals event
258        (
259            "world.wildlife.spawn.calendar.christmas.taiga.core_forest",
260            |c, col| {
261                close(c.temp, CONFIG.snow_temp + 0.2, 0.2) * col.tree_density * BASE_DENSITY * 0.4
262            },
263        ),
264        (
265            "world.wildlife.spawn.calendar.halloween.taiga.core",
266            |c, col| {
267                close(c.temp, CONFIG.snow_temp + 0.2, 0.2) * col.tree_density * BASE_DENSITY * 0.8
268            },
269        ),
270        (
271            "world.wildlife.spawn.calendar.april_fools.taiga.core",
272            |c, col| {
273                close(c.temp, CONFIG.snow_temp + 0.2, 0.2) * col.tree_density * BASE_DENSITY * 0.4
274            },
275        ),
276        (
277            "world.wildlife.spawn.calendar.easter.taiga.core",
278            |c, col| {
279                close(c.temp, CONFIG.snow_temp + 0.2, 0.2) * col.tree_density * BASE_DENSITY * 0.4
280            },
281        ),
282        // Core animals
283        ("world.wildlife.spawn.taiga.core", |c, _col| {
284            close(c.temp, CONFIG.snow_temp + 0.2, 0.2) * BASE_DENSITY * 1.0
285        }),
286        // Forest area animals
287        ("world.wildlife.spawn.taiga.forest", |c, col| {
288            close(c.temp, CONFIG.snow_temp + 0.2, 0.6) * col.tree_density * BASE_DENSITY * 0.9
289        }),
290        // Area animals
291        ("world.wildlife.spawn.taiga.area", |c, _col| {
292            close(c.temp, CONFIG.snow_temp + 0.2, 0.6) * BASE_DENSITY * 5.0
293        }),
294        // Water animals
295        ("world.wildlife.spawn.taiga.water", |c, col| {
296            close(c.temp, CONFIG.snow_temp, 0.15) * col.tree_density * BASE_DENSITY * 5.0
297        }),
298        // River wildlife
299        ("world.wildlife.spawn.taiga.river", |c, col| {
300            close(col.temp, CONFIG.snow_temp + 0.2, 0.6)
301                * if col.water_dist.map(|d| d < 1.0).unwrap_or(false)
302                    && !matches!(col.chunk.get_biome(), BiomeKind::Ocean)
303                    && c.alt > CONFIG.sea_level + 20.0
304                {
305                    0.001
306                } else {
307                    0.0
308                }
309        }),
310        // **Temperate**
311        // Area rare
312        ("world.wildlife.spawn.temperate.rare", |c, _col| {
313            close(c.temp, CONFIG.temperate_temp, 0.8) * BASE_DENSITY * 0.08
314        }),
315        // Plains
316        ("world.wildlife.spawn.temperate.plains", |c, _col| {
317            close(c.temp, CONFIG.temperate_temp, 0.8)
318                * close(c.tree_density, 0.0, 0.1)
319                * BASE_DENSITY
320                * 5.0
321        }),
322        // River wildlife
323        ("world.wildlife.spawn.temperate.river", |c, col| {
324            close(col.temp, CONFIG.temperate_temp, 0.6)
325                * if col.water_dist.map(|d| d < 1.0).unwrap_or(false)
326                    && !matches!(col.chunk.get_biome(), BiomeKind::Ocean)
327                    && c.alt > CONFIG.sea_level + 20.0
328                {
329                    0.001
330                } else {
331                    0.0
332                }
333        }),
334        // Forest animals
335        ("world.wildlife.spawn.temperate.wood", |c, col| {
336            close(c.temp, CONFIG.temperate_temp + 0.1, 0.5) * col.tree_density * BASE_DENSITY * 5.0
337        }),
338        // Rainforest animals
339        ("world.wildlife.spawn.temperate.rainforest", |c, _col| {
340            close(c.temp, CONFIG.temperate_temp + 0.1, 0.6)
341                * close(c.humidity, CONFIG.forest_hum, 0.6)
342                * BASE_DENSITY
343                * 5.0
344        }),
345        // Temperate Rainforest animals event
346        (
347            "world.wildlife.spawn.calendar.halloween.temperate.rainforest",
348            |c, _col| {
349                close(c.temp, CONFIG.temperate_temp + 0.1, 0.6)
350                    * close(c.humidity, CONFIG.forest_hum, 0.6)
351                    * BASE_DENSITY
352                    * 5.0
353            },
354        ),
355        (
356            "world.wildlife.spawn.calendar.april_fools.temperate.rainforest",
357            |c, _col| {
358                close(c.temp, CONFIG.temperate_temp + 0.1, 0.6)
359                    * close(c.humidity, CONFIG.forest_hum, 0.6)
360                    * BASE_DENSITY
361                    * 4.0
362            },
363        ),
364        (
365            "world.wildlife.spawn.calendar.easter.temperate.rainforest",
366            |c, _col| {
367                close(c.temp, CONFIG.temperate_temp + 0.1, 0.6)
368                    * close(c.humidity, CONFIG.forest_hum, 0.6)
369                    * BASE_DENSITY
370                    * 4.0
371            },
372        ),
373        // Ocean animals
374        ("world.wildlife.spawn.temperate.ocean", |_c, col| {
375            close(col.temp, CONFIG.temperate_temp, 1.0) / 10.0
376                * if col.water_dist.map(|d| d < 1.0).unwrap_or(false)
377                    && matches!(col.chunk.get_biome(), BiomeKind::Ocean)
378                {
379                    0.001
380                } else {
381                    0.0
382                }
383        }),
384        // Ocean beach animals
385        ("world.wildlife.spawn.temperate.beach", |c, col| {
386            close(col.temp, CONFIG.temperate_temp, 1.0) / 10.0
387                * if col.water_dist.map(|d| d < 30.0).unwrap_or(false)
388                    && !matches!(col.chunk.get_biome(), BiomeKind::Ocean)
389                    && c.alt < CONFIG.sea_level + 2.0
390                {
391                    0.001
392                } else {
393                    0.0
394                }
395        }),
396        // **Jungle**
397        // Rainforest animals
398        ("world.wildlife.spawn.jungle.rainforest", |c, _col| {
399            close(c.temp, CONFIG.tropical_temp + 0.2, 0.2)
400                * close(c.humidity, CONFIG.jungle_hum, 0.2)
401                * BASE_DENSITY
402                * 2.8
403        }),
404        // Rainforest area animals
405        ("world.wildlife.spawn.jungle.rainforest_area", |c, _col| {
406            close(c.temp, CONFIG.tropical_temp + 0.2, 0.3)
407                * close(c.humidity, CONFIG.jungle_hum, 0.2)
408                * BASE_DENSITY
409                * 8.0
410        }),
411        // Jungle animals event
412        (
413            "world.wildlife.spawn.calendar.halloween.jungle.area",
414            |c, _col| {
415                close(c.temp, CONFIG.tropical_temp + 0.2, 0.3)
416                    * close(c.humidity, CONFIG.jungle_hum, 0.2)
417                    * BASE_DENSITY
418                    * 10.0
419            },
420        ),
421        (
422            "world.wildlife.spawn.calendar.april_fools.jungle.area",
423            |c, _col| {
424                close(c.temp, CONFIG.tropical_temp + 0.2, 0.3)
425                    * close(c.humidity, CONFIG.jungle_hum, 0.2)
426                    * BASE_DENSITY
427                    * 8.0
428            },
429        ),
430        (
431            "world.wildlife.spawn.calendar.easter.jungle.area",
432            |c, _col| {
433                close(c.temp, CONFIG.tropical_temp + 0.2, 0.3)
434                    * close(c.humidity, CONFIG.jungle_hum, 0.2)
435                    * BASE_DENSITY
436                    * 8.0
437            },
438        ),
439        // **Tropical**
440        // River animals
441        ("world.wildlife.spawn.tropical.river", |c, col| {
442            close(col.temp, CONFIG.tropical_temp, 0.5)
443                * if col.water_dist.map(|d| d < 1.0).unwrap_or(false)
444                    && !matches!(col.chunk.get_biome(), BiomeKind::Ocean)
445                    && c.alt > CONFIG.sea_level + 20.0
446                {
447                    0.001
448                } else {
449                    0.0
450                }
451        }),
452        // Ocean animals
453        ("world.wildlife.spawn.tropical.ocean", |_c, col| {
454            close(col.temp, CONFIG.tropical_temp, 0.1) / 10.0
455                * if col.water_dist.map(|d| d < 1.0).unwrap_or(false)
456                    && matches!(col.chunk.get_biome(), BiomeKind::Ocean)
457                {
458                    0.001
459                } else {
460                    0.0
461                }
462        }),
463        // Ocean beach animals
464        ("world.wildlife.spawn.tropical.beach", |c, col| {
465            close(col.temp, CONFIG.tropical_temp, 1.0) / 10.0
466                * if col.water_dist.map(|d| d < 30.0).unwrap_or(false)
467                    && !matches!(col.chunk.get_biome(), BiomeKind::Ocean)
468                    && c.alt < CONFIG.sea_level + 2.0
469                {
470                    0.001
471                } else {
472                    0.0
473                }
474        }),
475        // Arctic ocean animals
476        ("world.wildlife.spawn.arctic.ocean", |_c, col| {
477            close(col.temp, CONFIG.snow_temp, 0.25) / 10.0
478                * if matches!(col.chunk.get_biome(), BiomeKind::Ocean) {
479                    0.001
480                } else {
481                    0.0
482                }
483        }),
484        // Rainforest area animals
485        ("world.wildlife.spawn.tropical.rainforest", |c, _col| {
486            close(c.temp, CONFIG.tropical_temp + 0.1, 0.4)
487                * close(c.humidity, CONFIG.jungle_hum, 0.4)
488                * BASE_DENSITY
489                * 2.0
490        }),
491        // Tropical Rainforest animals event
492        (
493            "world.wildlife.spawn.calendar.halloween.tropical.rainforest",
494            |c, _col| {
495                close(c.temp, CONFIG.tropical_temp + 0.1, 0.4)
496                    * close(c.humidity, CONFIG.jungle_hum, 0.4)
497                    * BASE_DENSITY
498                    * 3.5
499            },
500        ),
501        (
502            "world.wildlife.spawn.calendar.april_fools.tropical.rainforest",
503            |c, _col| {
504                close(c.temp, CONFIG.tropical_temp + 0.1, 0.4)
505                    * close(c.humidity, CONFIG.jungle_hum, 0.4)
506                    * BASE_DENSITY
507                    * 2.0
508            },
509        ),
510        // Rock animals
511        ("world.wildlife.spawn.tropical.rock", |c, col| {
512            close(c.temp, CONFIG.tropical_temp + 0.1, 0.5) * col.rock_density * BASE_DENSITY * 5.0
513        }),
514        // **Desert**
515        // Area animals
516        ("world.wildlife.spawn.desert.area", |c, _col| {
517            close(c.temp, CONFIG.tropical_temp + 0.1, 0.4)
518                * close(c.humidity, CONFIG.desert_hum, 0.4)
519                * BASE_DENSITY
520                * 0.8
521        }),
522        // Wasteland animals
523        ("world.wildlife.spawn.desert.wasteland", |c, _col| {
524            close(c.temp, CONFIG.desert_temp + 0.2, 0.3)
525                * close(c.humidity, CONFIG.desert_hum, 0.5)
526                * BASE_DENSITY
527                * 1.3
528        }),
529        // River animals
530        ("world.wildlife.spawn.desert.river", |c, col| {
531            close(col.temp, CONFIG.desert_temp + 0.2, 0.3)
532                * if col.water_dist.map(|d| d < 1.0).unwrap_or(false)
533                    && !matches!(col.chunk.get_biome(), BiomeKind::Ocean)
534                    && c.alt > CONFIG.sea_level + 20.0
535                {
536                    0.001
537                } else {
538                    0.0
539                }
540        }),
541        // Hot area desert
542        ("world.wildlife.spawn.desert.hot", |c, _col| {
543            close(c.temp, CONFIG.desert_temp + 0.2, 0.3) * BASE_DENSITY * 3.8
544        }),
545        // Rock animals
546        ("world.wildlife.spawn.desert.rock", |c, col| {
547            close(c.temp, CONFIG.desert_temp + 0.2, 0.05) * col.rock_density * BASE_DENSITY * 4.0
548        }),
549    ]
550}
551
552pub fn apply_wildlife_supplement<'a, R: Rng>(
553    // NOTE: Used only for dynamic elements like chests and entities!
554    dynamic_rng: &mut R,
555    wpos2d: Vec2<i32>,
556    mut get_column: impl FnMut(Vec2<i32>) -> Option<&'a ColumnSample<'a>>,
557    vol: &(impl RectSizedVol<Vox = Block> + ReadVol + WriteVol),
558    index: IndexRef,
559    chunk: &SimChunk,
560    supplement: &mut ChunkSupplement,
561    time: Option<&(TimeOfDay, Calendar)>,
562) {
563    let scatter = &index.wildlife_spawns;
564    // Configurable density multiplier
565    let wildlife_density_modifier = index.features.wildlife_density;
566
567    for y in 0..vol.size_xy().y as i32 {
568        for x in 0..vol.size_xy().x as i32 {
569            let offs = Vec2::new(x, y);
570
571            let wpos2d = wpos2d + offs;
572
573            // Sample terrain
574            let col_sample = if let Some(col_sample) = get_column(offs) {
575                col_sample
576            } else {
577                continue;
578            };
579
580            let is_underwater = col_sample.water_level > col_sample.alt;
581            let is_ice = col_sample.ice_depth > 0.5 && is_underwater;
582            let (current_day_period, calendar) = if let Some((time, calendar)) = time {
583                (DayPeriod::from(time.0), Some(calendar))
584            } else {
585                (DayPeriod::Noon, None)
586            };
587
588            let entity_group = scatter
589                .iter()
590                .filter_map(|(entry, get_density)| {
591                    let density = get_density(chunk, col_sample) * wildlife_density_modifier;
592                    (density > 0.0)
593                        .then(|| {
594                            entry
595                                .read()
596                                .request(current_day_period, calendar, is_underwater, is_ice)
597                                .and_then(|pack| {
598                                    (dynamic_rng.gen::<f32>() < density * col_sample.spawn_rate
599                                        && col_sample.gradient < Some(1.3))
600                                    .then_some(pack)
601                                })
602                        })
603                        .flatten()
604                })
605                .collect::<Vec<_>>() // TODO: Don't allocate
606                .choose(dynamic_rng)
607                .cloned();
608
609            if let Some(pack) = entity_group {
610                let desired_alt = match pack.spawn_mode {
611                    SpawnMode::Land | SpawnMode::Underwater => col_sample.alt,
612                    SpawnMode::Ice => col_sample.water_level + 1.0 + col_sample.ice_depth,
613                    SpawnMode::Water => dynamic_rng.gen_range(
614                        col_sample.alt..col_sample.water_level.max(col_sample.alt + 0.1),
615                    ),
616                    SpawnMode::Air(height) => {
617                        col_sample.alt.max(col_sample.water_level)
618                            + dynamic_rng.gen::<f32>() * height
619                    },
620                };
621
622                let (entity, group_size) = pack.generate(
623                    (wpos2d.map(|e| e as f32) + 0.5).with_z(desired_alt),
624                    dynamic_rng,
625                );
626                for e in 0..group_size {
627                    // Choose a nearby position
628                    let offs_wpos2d = (Vec2::new(
629                        (e as f32 / group_size as f32 * 2.0 * f32::consts::PI).sin(),
630                        (e as f32 / group_size as f32 * 2.0 * f32::consts::PI).cos(),
631                    ) * (5.0 + dynamic_rng.gen::<f32>().powf(0.5) * 5.0))
632                        .map(|e| e as i32);
633                    // Clamp position to chunk
634                    let offs_wpos2d = (offs + offs_wpos2d)
635                        .clamped(Vec2::zero(), vol.size_xy().map(|e| e as i32) - 1)
636                        - offs;
637
638                    // Find the intersection between ground and air, if there is one near the
639                    // surface
640                    let z_offset = (0..16)
641                        .map(|z| if z % 2 == 0 { z } else { -z } / 2)
642                        .find(|z| {
643                            (0..2).all(|z2| {
644                                vol.get(
645                                    Vec3::new(offs.x, offs.y, desired_alt as i32)
646                                        + offs_wpos2d.with_z(z + z2),
647                                )
648                                .map(|b| !b.is_solid())
649                                .unwrap_or(true)
650                            })
651                        });
652
653                    if let Some(z_offset) = z_offset {
654                        let mut entity = entity.clone();
655                        entity.pos += offs_wpos2d.with_z(z_offset).map(|e| e as f32);
656                        supplement.add_entity(entity);
657                    }
658                }
659            }
660        }
661    }
662}
663
664#[cfg(test)]
665mod tests {
666    use super::*;
667    use hashbrown::HashMap;
668
669    // Checks that each entry in spawn manifest is loadable
670    #[test]
671    fn test_load_entries() {
672        let scatter = spawn_manifest();
673        for (entry, _) in scatter.into_iter() {
674            drop(SpawnEntry::from(entry));
675        }
676    }
677
678    // Check that each spawn entry has unique name
679    #[test]
680    fn test_name_uniqueness() {
681        let scatter = spawn_manifest();
682        let mut names = HashMap::new();
683        for (entry, _) in scatter.into_iter() {
684            let SpawnEntry { name, .. } = SpawnEntry::from(entry);
685            if let Some(old_entry) = names.insert(name, entry) {
686                panic!("{}: Found name duplicate with {}", entry, old_entry);
687            }
688        }
689    }
690
691    // Checks that each entity is loadable
692    #[test]
693    fn test_load_entities() {
694        let scatter = spawn_manifest();
695        for (entry, _) in scatter.into_iter() {
696            let SpawnEntry { rules, .. } = SpawnEntry::from(entry);
697            for pack in rules {
698                let Pack { groups, .. } = pack;
699                for group in &groups {
700                    println!("{}:", entry);
701                    let (_, (_, _, asset)) = group;
702                    let dummy_pos = Vec3::new(0.0, 0.0, 0.0);
703                    let mut dummy_rng = thread_rng();
704                    let entity =
705                        EntityInfo::at(dummy_pos).with_asset_expect(asset, &mut dummy_rng, None);
706                    drop(entity);
707                }
708            }
709        }
710    }
711
712    // Checks that group distribution has valid form
713    #[test]
714    fn test_group_choose() {
715        let scatter = spawn_manifest();
716        for (entry, _) in scatter.into_iter() {
717            let SpawnEntry { rules, .. } = SpawnEntry::from(entry);
718            for pack in rules {
719                let Pack { groups, .. } = pack;
720                let dynamic_rng = &mut thread_rng();
721                let _ = groups
722                    .choose_weighted(dynamic_rng, |(p, _group)| *p)
723                    .unwrap_or_else(|err| {
724                        panic!("{}: Failed to choose random group. Err: {}", entry, err)
725                    });
726            }
727        }
728    }
729}