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 pub name: String,
24 pub note: String,
25 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#[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>>, }
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; vec![
151 ("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 ("world.wildlife.spawn.tundra.core", |c, _col| {
158 close(c.temp, CONFIG.snow_temp, 0.15) * BASE_DENSITY * 0.5
159 }),
160 (
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 ("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 (
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 ("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 ("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 (
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 ("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 (
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 ("world.wildlife.spawn.taiga.core", |c, _col| {
283 close(c.temp, CONFIG.snow_temp + 0.2, 0.2) * BASE_DENSITY * 1.0
284 }),
285 ("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 ("world.wildlife.spawn.taiga.area", |c, _col| {
291 close(c.temp, CONFIG.snow_temp + 0.2, 0.6) * BASE_DENSITY * 5.0
292 }),
293 ("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 ("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 ("world.wildlife.spawn.temperate.rare", |c, _col| {
312 close(c.temp, CONFIG.temperate_temp, 0.8) * BASE_DENSITY * 0.08
313 }),
314 ("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 ("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 ("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 ("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 (
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 ("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 ("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 ("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 ("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 (
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 ("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 ("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 ("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 ("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 ("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 (
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 ("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 ("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 ("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 ("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 ("world.wildlife.spawn.desert.hot", |c, _col| {
542 close(c.temp, CONFIG.desert_temp + 0.2, 0.3) * BASE_DENSITY * 3.8
543 }),
544 ("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 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 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 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<_>>() .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 let offs_wpos2d = (offs + offs_wpos2d)
625 .clamped(Vec2::zero(), vol.size_xy().map(|e| e as i32) - 1)
626 - offs;
627
628 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 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 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 #[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 #[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 #[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 #[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}