veloren_world/layer/
wildlife.rs

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