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, EntitySpawn},
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, iter};
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) -> EntitySpawn {
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        if group_size > 1 {
134            let group = iter::repeat_n(entity, group_size as usize).collect::<Vec<_>>();
135
136            EntitySpawn::Group(group)
137        } else {
138            EntitySpawn::Entity(Box::new(entity))
139        }
140    }
141}
142
143pub type DensityFn = fn(&SimChunk, &ColumnSample) -> f32;
144
145pub fn spawn_manifest() -> Vec<(&'static str, DensityFn)> {
146    const BASE_DENSITY: f32 = 1.0e-5; // Base wildlife density
147    // NOTE: Order matters.
148    // Entries with more specific requirements
149    // and overall scarcity should come first, where possible.
150    vec![
151        // **Tundra**
152        // Rock animals
153        ("world.wildlife.spawn.tundra.rock", |c, col| {
154            close(c.temp, CONFIG.snow_temp, 0.15) * BASE_DENSITY * col.rock_density * 1.0
155        }),
156        // Core animals
157        ("world.wildlife.spawn.tundra.core", |c, _col| {
158            close(c.temp, CONFIG.snow_temp, 0.15) * BASE_DENSITY * 0.5
159        }),
160        // Core animals events
161        (
162            "world.wildlife.spawn.calendar.christmas.tundra.core",
163            |c, _col| close(c.temp, CONFIG.snow_temp, 0.15) * BASE_DENSITY * 0.5,
164        ),
165        (
166            "world.wildlife.spawn.calendar.halloween.tundra.core",
167            |c, _col| close(c.temp, CONFIG.snow_temp, 0.15) * BASE_DENSITY * 1.0,
168        ),
169        (
170            "world.wildlife.spawn.calendar.april_fools.tundra.core",
171            |c, _col| close(c.temp, CONFIG.snow_temp, 0.15) * BASE_DENSITY * 0.5,
172        ),
173        (
174            "world.wildlife.spawn.calendar.easter.tundra.core",
175            |c, _col| close(c.temp, CONFIG.snow_temp, 0.15) * BASE_DENSITY * 0.5,
176        ),
177        // Snowy animals
178        ("world.wildlife.spawn.tundra.snow", |c, col| {
179            close(c.temp, CONFIG.snow_temp, 0.3) * BASE_DENSITY * col.snow_cover as i32 as f32 * 1.0
180        }),
181        // Snowy animals event
182        (
183            "world.wildlife.spawn.calendar.christmas.tundra.snow",
184            |c, col| {
185                close(c.temp, CONFIG.snow_temp, 0.3)
186                    * BASE_DENSITY
187                    * col.snow_cover as i32 as f32
188                    * 1.0
189            },
190        ),
191        (
192            "world.wildlife.spawn.calendar.halloween.tundra.snow",
193            |c, col| {
194                close(c.temp, CONFIG.snow_temp, 0.3)
195                    * BASE_DENSITY
196                    * col.snow_cover as i32 as f32
197                    * 1.5
198            },
199        ),
200        (
201            "world.wildlife.spawn.calendar.april_fools.tundra.snow",
202            |c, col| {
203                close(c.temp, CONFIG.snow_temp, 0.3)
204                    * BASE_DENSITY
205                    * col.snow_cover as i32 as f32
206                    * 1.0
207            },
208        ),
209        (
210            "world.wildlife.spawn.calendar.easter.tundra.snow",
211            |c, col| {
212                close(c.temp, CONFIG.snow_temp, 0.3)
213                    * BASE_DENSITY
214                    * col.snow_cover as i32 as f32
215                    * 1.0
216            },
217        ),
218        // Forest animals
219        ("world.wildlife.spawn.tundra.forest", |c, col| {
220            close(c.temp, CONFIG.snow_temp, 0.3) * col.tree_density * BASE_DENSITY * 1.4
221        }),
222        // River wildlife
223        ("world.wildlife.spawn.tundra.river", |c, col| {
224            close(col.temp, CONFIG.snow_temp, 0.3)
225                * if col.water_dist.map(|d| d < 1.0).unwrap_or(false)
226                    && !matches!(col.chunk.get_biome(), BiomeKind::Ocean)
227                    && c.alt > CONFIG.sea_level + 20.0
228                {
229                    0.001
230                } else {
231                    0.0
232                }
233        }),
234        // Forest animals event
235        (
236            "world.wildlife.spawn.calendar.christmas.tundra.forest",
237            |c, col| close(c.temp, CONFIG.snow_temp, 0.3) * col.tree_density * BASE_DENSITY * 1.4,
238        ),
239        (
240            "world.wildlife.spawn.calendar.halloween.tundra.forest",
241            |c, col| close(c.temp, CONFIG.snow_temp, 0.3) * col.tree_density * BASE_DENSITY * 2.0,
242        ),
243        (
244            "world.wildlife.spawn.calendar.april_fools.tundra.forest",
245            |c, col| close(c.temp, CONFIG.snow_temp, 0.3) * col.tree_density * BASE_DENSITY * 1.4,
246        ),
247        (
248            "world.wildlife.spawn.calendar.easter.tundra.forest",
249            |c, col| close(c.temp, CONFIG.snow_temp, 0.3) * col.tree_density * BASE_DENSITY * 1.4,
250        ),
251        // **Taiga**
252        // Forest core animals
253        ("world.wildlife.spawn.taiga.core_forest", |c, col| {
254            close(c.temp, CONFIG.snow_temp + 0.2, 0.2) * col.tree_density * BASE_DENSITY * 0.4
255        }),
256        // Forest core animals event
257        (
258            "world.wildlife.spawn.calendar.christmas.taiga.core_forest",
259            |c, col| {
260                close(c.temp, CONFIG.snow_temp + 0.2, 0.2) * col.tree_density * BASE_DENSITY * 0.4
261            },
262        ),
263        (
264            "world.wildlife.spawn.calendar.halloween.taiga.core",
265            |c, col| {
266                close(c.temp, CONFIG.snow_temp + 0.2, 0.2) * col.tree_density * BASE_DENSITY * 0.8
267            },
268        ),
269        (
270            "world.wildlife.spawn.calendar.april_fools.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        (
276            "world.wildlife.spawn.calendar.easter.taiga.core",
277            |c, col| {
278                close(c.temp, CONFIG.snow_temp + 0.2, 0.2) * col.tree_density * BASE_DENSITY * 0.4
279            },
280        ),
281        // Core animals
282        ("world.wildlife.spawn.taiga.core", |c, _col| {
283            close(c.temp, CONFIG.snow_temp + 0.2, 0.2) * BASE_DENSITY * 1.0
284        }),
285        // Forest area animals
286        ("world.wildlife.spawn.taiga.forest", |c, col| {
287            close(c.temp, CONFIG.snow_temp + 0.2, 0.6) * col.tree_density * BASE_DENSITY * 0.9
288        }),
289        // Area animals
290        ("world.wildlife.spawn.taiga.area", |c, _col| {
291            close(c.temp, CONFIG.snow_temp + 0.2, 0.6) * BASE_DENSITY * 5.0
292        }),
293        // Water animals
294        ("world.wildlife.spawn.taiga.water", |c, col| {
295            close(c.temp, CONFIG.snow_temp, 0.15) * col.tree_density * BASE_DENSITY * 5.0
296        }),
297        // River wildlife
298        ("world.wildlife.spawn.taiga.river", |c, col| {
299            close(col.temp, CONFIG.snow_temp + 0.2, 0.6)
300                * if col.water_dist.map(|d| d < 1.0).unwrap_or(false)
301                    && !matches!(col.chunk.get_biome(), BiomeKind::Ocean)
302                    && c.alt > CONFIG.sea_level + 20.0
303                {
304                    0.001
305                } else {
306                    0.0
307                }
308        }),
309        // **Temperate**
310        // Area rare
311        ("world.wildlife.spawn.temperate.rare", |c, _col| {
312            close(c.temp, CONFIG.temperate_temp, 0.8) * BASE_DENSITY * 0.08
313        }),
314        // Plains
315        ("world.wildlife.spawn.temperate.plains", |c, _col| {
316            close(c.temp, CONFIG.temperate_temp, 0.8)
317                * close(c.tree_density, 0.0, 0.1)
318                * BASE_DENSITY
319                * 5.0
320        }),
321        // River wildlife
322        ("world.wildlife.spawn.temperate.river", |c, col| {
323            close(col.temp, CONFIG.temperate_temp, 0.6)
324                * if col.water_dist.map(|d| d < 1.0).unwrap_or(false)
325                    && !matches!(col.chunk.get_biome(), BiomeKind::Ocean)
326                    && c.alt > CONFIG.sea_level + 20.0
327                {
328                    0.001
329                } else {
330                    0.0
331                }
332        }),
333        // Forest animals
334        ("world.wildlife.spawn.temperate.wood", |c, col| {
335            close(c.temp, CONFIG.temperate_temp + 0.1, 0.5) * col.tree_density * BASE_DENSITY * 5.0
336        }),
337        // Rainforest animals
338        ("world.wildlife.spawn.temperate.rainforest", |c, _col| {
339            close(c.temp, CONFIG.temperate_temp + 0.1, 0.6)
340                * close(c.humidity, CONFIG.forest_hum, 0.6)
341                * BASE_DENSITY
342                * 5.0
343        }),
344        // Temperate Rainforest animals event
345        (
346            "world.wildlife.spawn.calendar.halloween.temperate.rainforest",
347            |c, _col| {
348                close(c.temp, CONFIG.temperate_temp + 0.1, 0.6)
349                    * close(c.humidity, CONFIG.forest_hum, 0.6)
350                    * BASE_DENSITY
351                    * 5.0
352            },
353        ),
354        (
355            "world.wildlife.spawn.calendar.april_fools.temperate.rainforest",
356            |c, _col| {
357                close(c.temp, CONFIG.temperate_temp + 0.1, 0.6)
358                    * close(c.humidity, CONFIG.forest_hum, 0.6)
359                    * BASE_DENSITY
360                    * 4.0
361            },
362        ),
363        (
364            "world.wildlife.spawn.calendar.easter.temperate.rainforest",
365            |c, _col| {
366                close(c.temp, CONFIG.temperate_temp + 0.1, 0.6)
367                    * close(c.humidity, CONFIG.forest_hum, 0.6)
368                    * BASE_DENSITY
369                    * 4.0
370            },
371        ),
372        // Ocean animals
373        ("world.wildlife.spawn.temperate.ocean", |_c, col| {
374            close(col.temp, CONFIG.temperate_temp, 1.0) / 10.0
375                * if col.water_dist.map(|d| d < 1.0).unwrap_or(false)
376                    && matches!(col.chunk.get_biome(), BiomeKind::Ocean)
377                {
378                    0.001
379                } else {
380                    0.0
381                }
382        }),
383        // Ocean beach animals
384        ("world.wildlife.spawn.temperate.beach", |c, col| {
385            close(col.temp, CONFIG.temperate_temp, 1.0) / 10.0
386                * if col.water_dist.map(|d| d < 30.0).unwrap_or(false)
387                    && !matches!(col.chunk.get_biome(), BiomeKind::Ocean)
388                    && c.alt < CONFIG.sea_level + 2.0
389                {
390                    0.001
391                } else {
392                    0.0
393                }
394        }),
395        // **Jungle**
396        // Rainforest animals
397        ("world.wildlife.spawn.jungle.rainforest", |c, _col| {
398            close(c.temp, CONFIG.tropical_temp + 0.2, 0.2)
399                * close(c.humidity, CONFIG.jungle_hum, 0.2)
400                * BASE_DENSITY
401                * 2.8
402        }),
403        // Rainforest area animals
404        ("world.wildlife.spawn.jungle.rainforest_area", |c, _col| {
405            close(c.temp, CONFIG.tropical_temp + 0.2, 0.3)
406                * close(c.humidity, CONFIG.jungle_hum, 0.2)
407                * BASE_DENSITY
408                * 8.0
409        }),
410        // Jungle animals event
411        (
412            "world.wildlife.spawn.calendar.halloween.jungle.area",
413            |c, _col| {
414                close(c.temp, CONFIG.tropical_temp + 0.2, 0.3)
415                    * close(c.humidity, CONFIG.jungle_hum, 0.2)
416                    * BASE_DENSITY
417                    * 10.0
418            },
419        ),
420        (
421            "world.wildlife.spawn.calendar.april_fools.jungle.area",
422            |c, _col| {
423                close(c.temp, CONFIG.tropical_temp + 0.2, 0.3)
424                    * close(c.humidity, CONFIG.jungle_hum, 0.2)
425                    * BASE_DENSITY
426                    * 8.0
427            },
428        ),
429        (
430            "world.wildlife.spawn.calendar.easter.jungle.area",
431            |c, _col| {
432                close(c.temp, CONFIG.tropical_temp + 0.2, 0.3)
433                    * close(c.humidity, CONFIG.jungle_hum, 0.2)
434                    * BASE_DENSITY
435                    * 8.0
436            },
437        ),
438        // **Tropical**
439        // River animals
440        ("world.wildlife.spawn.tropical.river", |c, col| {
441            close(col.temp, CONFIG.tropical_temp, 0.5)
442                * if col.water_dist.map(|d| d < 1.0).unwrap_or(false)
443                    && !matches!(col.chunk.get_biome(), BiomeKind::Ocean)
444                    && c.alt > CONFIG.sea_level + 20.0
445                {
446                    0.001
447                } else {
448                    0.0
449                }
450        }),
451        // Ocean animals
452        ("world.wildlife.spawn.tropical.ocean", |_c, col| {
453            close(col.temp, CONFIG.tropical_temp, 0.1) / 10.0
454                * if col.water_dist.map(|d| d < 1.0).unwrap_or(false)
455                    && matches!(col.chunk.get_biome(), BiomeKind::Ocean)
456                {
457                    0.001
458                } else {
459                    0.0
460                }
461        }),
462        // Ocean beach animals
463        ("world.wildlife.spawn.tropical.beach", |c, col| {
464            close(col.temp, CONFIG.tropical_temp, 1.0) / 10.0
465                * if col.water_dist.map(|d| d < 30.0).unwrap_or(false)
466                    && !matches!(col.chunk.get_biome(), BiomeKind::Ocean)
467                    && c.alt < CONFIG.sea_level + 2.0
468                {
469                    0.001
470                } else {
471                    0.0
472                }
473        }),
474        // Arctic ocean animals
475        ("world.wildlife.spawn.arctic.ocean", |_c, col| {
476            close(col.temp, CONFIG.snow_temp, 0.25) / 10.0
477                * if matches!(col.chunk.get_biome(), BiomeKind::Ocean) {
478                    0.001
479                } else {
480                    0.0
481                }
482        }),
483        // Rainforest area animals
484        ("world.wildlife.spawn.tropical.rainforest", |c, _col| {
485            close(c.temp, CONFIG.tropical_temp + 0.1, 0.4)
486                * close(c.humidity, CONFIG.jungle_hum, 0.4)
487                * BASE_DENSITY
488                * 2.0
489        }),
490        // Tropical Rainforest animals event
491        (
492            "world.wildlife.spawn.calendar.halloween.tropical.rainforest",
493            |c, _col| {
494                close(c.temp, CONFIG.tropical_temp + 0.1, 0.4)
495                    * close(c.humidity, CONFIG.jungle_hum, 0.4)
496                    * BASE_DENSITY
497                    * 3.5
498            },
499        ),
500        (
501            "world.wildlife.spawn.calendar.april_fools.tropical.rainforest",
502            |c, _col| {
503                close(c.temp, CONFIG.tropical_temp + 0.1, 0.4)
504                    * close(c.humidity, CONFIG.jungle_hum, 0.4)
505                    * BASE_DENSITY
506                    * 2.0
507            },
508        ),
509        // Rock animals
510        ("world.wildlife.spawn.tropical.rock", |c, col| {
511            close(c.temp, CONFIG.tropical_temp + 0.1, 0.5) * col.rock_density * BASE_DENSITY * 5.0
512        }),
513        // **Desert**
514        // Area animals
515        ("world.wildlife.spawn.desert.area", |c, _col| {
516            close(c.temp, CONFIG.desert_temp + 0.1, 0.4)
517                * close(c.humidity, CONFIG.desert_hum, 0.4)
518                * BASE_DENSITY
519                * 0.8
520        }),
521        // Wasteland animals
522        ("world.wildlife.spawn.desert.wasteland", |c, _col| {
523            close(c.temp, CONFIG.desert_temp + 0.2, 0.3)
524                * close(c.humidity, CONFIG.desert_hum, 0.5)
525                * BASE_DENSITY
526                * 1.3
527        }),
528        // River animals
529        ("world.wildlife.spawn.desert.river", |c, col| {
530            close(col.temp, CONFIG.desert_temp + 0.2, 0.3)
531                * if col.water_dist.map(|d| d < 1.0).unwrap_or(false)
532                    && !matches!(col.chunk.get_biome(), BiomeKind::Ocean)
533                    && c.alt > CONFIG.sea_level + 20.0
534                {
535                    0.001
536                } else {
537                    0.0
538                }
539        }),
540        // Hot area desert
541        ("world.wildlife.spawn.desert.hot", |c, _col| {
542            close(c.temp, CONFIG.desert_temp + 0.2, 0.3) * BASE_DENSITY * 3.8
543        }),
544        // Rock animals
545        ("world.wildlife.spawn.desert.rock", |c, col| {
546            close(c.temp, CONFIG.desert_temp + 0.2, 0.05) * col.rock_density * BASE_DENSITY * 4.0
547        }),
548    ]
549}
550
551pub fn apply_wildlife_supplement<'a, R: Rng>(
552    // NOTE: Used only for dynamic elements like chests and entities!
553    dynamic_rng: &mut R,
554    wpos2d: Vec2<i32>,
555    mut get_column: impl FnMut(Vec2<i32>) -> Option<&'a ColumnSample<'a>>,
556    vol: &(impl RectSizedVol<Vox = Block> + ReadVol + WriteVol),
557    index: IndexRef,
558    chunk: &SimChunk,
559    supplement: &mut ChunkSupplement,
560    time: Option<&(TimeOfDay, Calendar)>,
561) {
562    let scatter = &index.wildlife_spawns;
563    // Configurable density multiplier
564    let wildlife_density_modifier = index.features.wildlife_density;
565
566    for y in 0..vol.size_xy().y as i32 {
567        for x in 0..vol.size_xy().x as i32 {
568            let offs = Vec2::new(x, y);
569
570            let wpos2d = wpos2d + offs;
571
572            // Sample terrain
573            let col_sample = if let Some(col_sample) = get_column(offs) {
574                col_sample
575            } else {
576                continue;
577            };
578
579            let is_underwater = col_sample.water_level > col_sample.alt;
580            let is_ice = col_sample.ice_depth > 0.5 && is_underwater;
581            let (current_day_period, calendar) = if let Some((time, calendar)) = time {
582                (DayPeriod::from(time.0), Some(calendar))
583            } else {
584                (DayPeriod::Noon, None)
585            };
586
587            let entity_group = scatter
588                .iter()
589                .filter_map(|(entry, get_density)| {
590                    let density = get_density(chunk, col_sample) * wildlife_density_modifier;
591                    (density > 0.0)
592                        .then(|| {
593                            entry
594                                .read()
595                                .0
596                                .request(current_day_period, calendar, is_underwater, is_ice)
597                                .and_then(|pack| {
598                                    (dynamic_rng.random::<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_mut(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.random_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.random::<f32>() * height
619                    },
620                };
621
622                let spawn_offset = |offs_wpos2d: Vec2<i32>| {
623                    // Clamp position to chunk
624                    let offs_wpos2d = (offs + offs_wpos2d)
625                        .clamped(Vec2::zero(), vol.size_xy().map(|e| e as i32) - 1)
626                        - offs;
627
628                    // Find the intersection between ground and air, if there is one near the
629                    // surface
630                    let z_offset = (0..16)
631                        .map(|z| if z % 2 == 0 { z } else { -z } / 2)
632                        .find(|z| {
633                            (0..2).all(|z2| {
634                                vol.get(
635                                    Vec3::new(offs.x, offs.y, desired_alt as i32)
636                                        + offs_wpos2d.with_z(z + z2),
637                                )
638                                .map(|b| !b.is_solid())
639                                .unwrap_or(true)
640                            })
641                        });
642
643                    z_offset.map(|z_offset| offs_wpos2d.with_z(z_offset).map(|e| e as f32))
644                };
645
646                let mut entity_spawn = pack.generate(
647                    (wpos2d.map(|e| e as f32) + 0.5).with_z(desired_alt),
648                    dynamic_rng,
649                );
650                match entity_spawn {
651                    EntitySpawn::Entity(ref mut entity) => {
652                        // Choose a nearby position
653                        let offs_wpos2d = (Vec2::new(0.0, 1.0)
654                            * (5.0 + dynamic_rng.random::<f32>().powf(0.5) * 5.0))
655                            .map(|e| e as i32);
656
657                        if let Some(spawn_offset) = spawn_offset(offs_wpos2d) {
658                            entity.pos += spawn_offset;
659                            supplement.add_entity_spawn(entity_spawn);
660                        }
661                    },
662                    EntitySpawn::Group(ref mut group) => {
663                        let group_size = group.len();
664                        for e in (0..group.len()).rev() {
665                            // Choose a nearby position
666                            let offs_wpos2d = (Vec2::new(
667                                (e as f32 / group_size as f32 * 2.0 * f32::consts::PI).sin(),
668                                (e as f32 / group_size as f32 * 2.0 * f32::consts::PI).cos(),
669                            ) * (5.0
670                                + dynamic_rng.random::<f32>().powf(0.5) * 5.0))
671                                .map(|e| e as i32);
672
673                            if let Some(spawn_offset) = spawn_offset(offs_wpos2d) {
674                                group[e].pos += spawn_offset;
675                            } else {
676                                group.remove(e);
677                            }
678                        }
679
680                        if !group.is_empty() {
681                            supplement.add_entity_spawn(entity_spawn);
682                        }
683                    },
684                }
685            }
686        }
687    }
688}
689
690#[cfg(test)]
691mod tests {
692    use super::*;
693    use hashbrown::HashMap;
694
695    // Checks that each entry in spawn manifest is loadable
696    #[test]
697    fn test_load_entries() {
698        let scatter = spawn_manifest();
699        for (entry, _) in scatter.into_iter() {
700            drop(SpawnEntry::from(entry));
701        }
702    }
703
704    // Check that each spawn entry has unique name
705    #[test]
706    fn test_name_uniqueness() {
707        let scatter = spawn_manifest();
708        let mut names = HashMap::new();
709        for (entry, _) in scatter.into_iter() {
710            let SpawnEntry { name, .. } = SpawnEntry::from(entry);
711            if let Some(old_entry) = names.insert(name, entry) {
712                panic!("{}: Found name duplicate with {}", entry, old_entry);
713            }
714        }
715    }
716
717    // Checks that each entity is loadable
718    #[test]
719    fn test_load_entities() {
720        let scatter = spawn_manifest();
721        for (entry, _) in scatter.into_iter() {
722            let SpawnEntry { rules, .. } = SpawnEntry::from(entry);
723            for pack in rules {
724                let Pack { groups, .. } = pack;
725                for group in &groups {
726                    println!("{}:", entry);
727                    let (_, (_, _, asset)) = group;
728                    let dummy_pos = Vec3::new(0.0, 0.0, 0.0);
729                    let mut dummy_rng = rand::rng();
730                    let entity =
731                        EntityInfo::at(dummy_pos).with_asset_expect(asset, &mut dummy_rng, None);
732                    drop(entity);
733                }
734            }
735        }
736    }
737
738    // Checks that group distribution has valid form
739    #[test]
740    fn test_group_choose() {
741        let scatter = spawn_manifest();
742        for (entry, _) in scatter.into_iter() {
743            let SpawnEntry { rules, .. } = SpawnEntry::from(entry);
744            for pack in rules {
745                let Pack { groups, .. } = pack;
746                let dynamic_rng = &mut rand::rng();
747                let _ = groups
748                    .choose_weighted(dynamic_rng, |(p, _group)| *p)
749                    .unwrap_or_else(|err| {
750                        panic!("{}: Failed to choose random group. Err: {}", entry, err)
751                    });
752            }
753        }
754    }
755}