veloren_world/layer/
spot.rs

1use crate::{
2    Canvas,
3    sim::{SimChunk, WorldSim},
4    util::{Sampler, UnitChooser, seed_expan},
5};
6use common::{
7    generation::EntityInfo,
8    spot::{RON_SPOT_PROPERTIES, Spot, SpotCondition},
9    terrain::{BiomeKind, Structure, TerrainChunkSize},
10    vol::RectVolSize,
11};
12use rand::prelude::*;
13use rand_chacha::ChaChaRng;
14use std::ops::Range;
15use vek::*;
16
17pub trait SpotGenerate {
18    fn generate(world: &mut WorldSim);
19
20    fn generate_spots(
21        spot: Spot,
22        world: &mut WorldSim,
23        freq: f32,
24        valid: impl FnMut(f32, &SimChunk) -> bool,
25        spawn: bool,
26    );
27}
28
29impl SpotGenerate for Spot {
30    fn generate(world: &mut WorldSim) {
31        use BiomeKind::*;
32        // Trees/spawn: false => *No* trees around the spot
33        // Themed Spots -> Act as an introduction to themes of sites
34        for s in RON_SPOT_PROPERTIES.0.iter() {
35            Self::generate_spots(
36                Spot::RonFile(s),
37                world,
38                s.freq,
39                |g, c| is_valid(&s.condition, g, c),
40                s.spawn,
41            );
42        }
43        Self::generate_spots(
44            Spot::WitchHouse,
45            world,
46            1.0,
47            |g, c| {
48                g < 0.25
49                    && !c.near_cliffs()
50                    && !c.river.near_water()
51                    && !c.path.0.is_way()
52                    && c.sites.is_empty()
53                    && matches!(
54                        c.get_biome(),
55                        Grassland | Forest | Taiga | Snowland | Jungle
56                    )
57            },
58            false,
59        );
60        Self::generate_spots(
61            Spot::Igloo,
62            world,
63            2.0,
64            |g, c| {
65                g < 0.5
66                    && !c.near_cliffs()
67                    && !c.river.near_water()
68                    && !c.path.0.is_way()
69                    && c.sites.is_empty()
70                    && matches!(c.get_biome(), Snowland)
71            },
72            false,
73        );
74        Self::generate_spots(
75            Spot::SaurokAltar,
76            world,
77            1.0,
78            |g, c| {
79                g < 0.25
80                    && !c.near_cliffs()
81                    && !c.river.near_water()
82                    && !c.path.0.is_way()
83                    && c.sites.is_empty()
84                    && matches!(c.get_biome(), Jungle | Forest)
85            },
86            false,
87        );
88        Self::generate_spots(
89            Spot::SaurokTotem,
90            world,
91            1.0,
92            |g, c| {
93                g < 0.25
94                    && !c.near_cliffs()
95                    && !c.river.near_water()
96                    && !c.path.0.is_way()
97                    && c.sites.is_empty()
98                    && matches!(c.get_biome(), Jungle | Forest)
99            },
100            false,
101        );
102        Self::generate_spots(
103            Spot::JungleOutpost,
104            world,
105            1.0,
106            |g, c| {
107                g < 0.25
108                    && !c.near_cliffs()
109                    && !c.river.near_water()
110                    && !c.path.0.is_way()
111                    && c.sites.is_empty()
112                    && matches!(c.get_biome(), Jungle | Forest)
113            },
114            false,
115        );
116        Self::generate_spots(
117            Spot::JungleTemple,
118            world,
119            0.5,
120            |g, c| {
121                g < 0.25
122                    && !c.near_cliffs()
123                    && !c.river.near_water()
124                    && !c.path.0.is_way()
125                    && c.sites.is_empty()
126                    && matches!(c.get_biome(), Jungle | Forest)
127            },
128            false,
129        );
130        Self::generate_spots(
131            Spot::MyrmidonTemple,
132            world,
133            1.0,
134            |g, c| {
135                g < 0.1
136                    && !c.near_cliffs()
137                    && !c.river.near_water()
138                    && !c.path.0.is_way()
139                    && c.sites.is_empty()
140                    && matches!(c.get_biome(), Desert | Jungle)
141            },
142            false,
143        );
144        Self::generate_spots(
145            Spot::GnarlingTotem,
146            world,
147            1.5,
148            |g, c| {
149                g < 0.25
150                    && !c.near_cliffs()
151                    && !c.river.near_water()
152                    && !c.path.0.is_way()
153                    && c.sites.is_empty()
154                    && matches!(c.get_biome(), Forest | Grassland)
155            },
156            false,
157        );
158        Self::generate_spots(
159            Spot::FallenTree,
160            world,
161            1.5,
162            |g, c| {
163                g < 0.25
164                    && !c.near_cliffs()
165                    && !c.river.near_water()
166                    && !c.path.0.is_way()
167                    && c.sites.is_empty()
168                    && matches!(c.get_biome(), Forest | Grassland)
169            },
170            false,
171        );
172        // Random World Objects -> Themed to their Biome and the NPCs that regularly
173        // spawn there
174        Self::generate_spots(
175            Spot::LionRock,
176            world,
177            1.5,
178            |g, c| {
179                g < 0.25
180                    && !c.near_cliffs()
181                    && !c.river.near_water()
182                    && !c.path.0.is_way()
183                    && c.sites.is_empty()
184                    && matches!(c.get_biome(), Savannah)
185            },
186            false,
187        );
188        Self::generate_spots(
189            Spot::WolfBurrow,
190            world,
191            1.5,
192            |g, c| {
193                g < 0.25
194                    && !c.near_cliffs()
195                    && !c.river.near_water()
196                    && !c.path.0.is_way()
197                    && c.sites.is_empty()
198                    && matches!(c.get_biome(), Forest | Grassland)
199            },
200            false,
201        );
202        Self::generate_spots(
203            Spot::TreeStumpForest,
204            world,
205            20.0,
206            |g, c| {
207                g < 0.25
208                    && !c.near_cliffs()
209                    && !c.river.near_water()
210                    && !c.path.0.is_way()
211                    && c.sites.is_empty()
212                    && matches!(c.get_biome(), Jungle | Forest)
213            },
214            true,
215        );
216        Self::generate_spots(
217            Spot::DesertBones,
218            world,
219            6.0,
220            |g, c| {
221                g < 0.25
222                    && !c.near_cliffs()
223                    && !c.river.near_water()
224                    && !c.path.0.is_way()
225                    && c.sites.is_empty()
226                    && matches!(c.get_biome(), Desert)
227            },
228            false,
229        );
230        Self::generate_spots(
231            Spot::Arch,
232            world,
233            2.0,
234            |g, c| {
235                g < 0.25
236                    && !c.near_cliffs()
237                    && !c.river.near_water()
238                    && !c.path.0.is_way()
239                    && c.sites.is_empty()
240                    && matches!(c.get_biome(), Desert)
241            },
242            false,
243        );
244        Self::generate_spots(
245            Spot::AirshipCrash,
246            world,
247            0.7,
248            |g, c| {
249                g < 0.25
250                    && !c.near_cliffs()
251                    && !c.river.near_water()
252                    && !c.path.0.is_way()
253                    && c.sites.is_empty()
254                    && !matches!(c.get_biome(), Mountain | Void | Ocean)
255            },
256            false,
257        );
258        Self::generate_spots(
259            Spot::FruitTree,
260            world,
261            20.0,
262            |g, c| {
263                g < 0.25
264                    && !c.near_cliffs()
265                    && !c.river.near_water()
266                    && !c.path.0.is_way()
267                    && c.sites.is_empty()
268                    && matches!(c.get_biome(), Forest)
269            },
270            true,
271        );
272        Self::generate_spots(
273            Spot::GnomeSpring,
274            world,
275            1.0,
276            |g, c| {
277                g < 0.25
278                    && !c.near_cliffs()
279                    && !c.river.near_water()
280                    && !c.path.0.is_way()
281                    && c.sites.is_empty()
282                    && matches!(c.get_biome(), Forest)
283            },
284            false,
285        );
286        Self::generate_spots(
287            Spot::Shipwreck,
288            world,
289            1.0,
290            |g, c| {
291                g < 0.25 && c.is_underwater() && c.sites.is_empty() && c.water_alt > c.alt + 30.0
292            },
293            true,
294        );
295        Self::generate_spots(
296            Spot::Shipwreck2,
297            world,
298            1.0,
299            |g, c| {
300                g < 0.25 && c.is_underwater() && c.sites.is_empty() && c.water_alt > c.alt + 30.0
301            },
302            true,
303        );
304        // Small Grave
305        Self::generate_spots(
306            Spot::GraveSmall,
307            world,
308            2.0,
309            |g, c| {
310                g < 0.25
311                    && !c.near_cliffs()
312                    && !c.river.near_water()
313                    && !c.path.0.is_way()
314                    && c.sites.is_empty()
315                    && matches!(c.get_biome(), Forest | Taiga | Jungle | Grassland)
316            },
317            false,
318        );
319
320        // Missing:
321        /*
322        Bandit Camp
323        Hunter Camp
324        TowerRuinForest
325        TowerRuinDesert
326        WellOfLight
327        Merchant Outpost -> Near a road!
328        *Quirky:*
329        TreeHouse (Forest)
330        EnchantedRock (Forest, Jungle)
331        */
332    }
333
334    fn generate_spots(
335        // What kind of spot are we generating?
336        spot: Spot,
337        world: &mut WorldSim,
338        // How often should this spot appear (per square km, on average)?
339        freq: f32,
340        // What tests should we perform to see whether we can spawn the spot here? The two
341        // parameters are the gradient of the terrain and the [`SimChunk`] of the candidate
342        // location.
343        mut valid: impl FnMut(f32, &SimChunk) -> bool,
344        // Should we allow trees and other trivial structures to spawn close to the spot?
345        spawn: bool,
346    ) {
347        let world_size = world.get_size();
348        for _ in
349            0..(world_size.product() as f32 * TerrainChunkSize::RECT_SIZE.product() as f32 * freq
350                / 1000.0f32.powi(2))
351            .ceil() as u64
352        {
353            let pos = world_size.map(|e| (world.rng.gen_range(0..e) & !0b11) as i32);
354            if let Some((_, chunk)) = world
355                .get_gradient_approx(pos)
356                .zip(world.get_mut(pos))
357                .filter(|(grad, chunk)| valid(*grad, chunk))
358            {
359                chunk.spot = Some(spot);
360                if !spawn {
361                    chunk.tree_density = 0.0;
362                    chunk.spawn_rate = 0.0;
363                }
364            }
365        }
366    }
367}
368
369#[derive(Default)]
370struct SpotConfig<'a> {
371    // The manifest containing a list of possible base structures for the spot (one will be
372    // chosen)
373    base_structures: Option<&'a str>,
374    // The maximum distance from the centre of the spot that entities will spawn
375    entity_radius: f32,
376    // The entities that should be spawned in the spot, from closest to furthest
377    // (count_range, spec)
378    // count_range = number of entities, chosen randomly within this range (not inclusive!)
379    // spec = Manifest spec for the entity kind
380    entities: &'a [(Range<i32>, &'a str)],
381}
382fn spot_config(spot: &Spot) -> SpotConfig {
383    match spot {
384        // Themed Spots
385        Spot::DwarvenGrave => SpotConfig {
386            base_structures: Some("spots_grasslands.dwarven_grave"),
387            entity_radius: 60.0,
388            entities: &[(6..12, "common.entity.spot.dwarf_grave_robber")],
389        },
390        Spot::SaurokAltar => SpotConfig {
391            base_structures: Some("spots.jungle.saurok-altar"),
392            entity_radius: 12.0,
393            entities: &[
394                (0..3, "common.entity.wild.aggressive.occult_saurok"),
395                (0..3, "common.entity.wild.aggressive.sly_saurok"),
396                (0..3, "common.entity.wild.aggressive.mighty_saurok"),
397            ],
398        },
399        Spot::SaurokTotem => SpotConfig {
400            base_structures: Some("spots.jungle.saurok_totem"),
401            entity_radius: 20.0,
402            entities: &[
403                (0..3, "common.entity.wild.aggressive.occult_saurok"),
404                (0..3, "common.entity.wild.aggressive.sly_saurok"),
405                (0..3, "common.entity.wild.aggressive.mighty_saurok"),
406            ],
407        },
408        Spot::JungleOutpost => SpotConfig {
409            base_structures: Some("spots.jungle.outpost"),
410            entity_radius: 40.0,
411            entities: &[(6..12, "common.entity.spot.grim_salvager")],
412        },
413        Spot::JungleTemple => SpotConfig {
414            base_structures: Some("spots.jungle.temple_small"),
415            entity_radius: 40.0,
416            entities: &[
417                (2..8, "common.entity.wild.aggressive.occult_saurok"),
418                (2..8, "common.entity.wild.aggressive.sly_saurok"),
419                (2..8, "common.entity.wild.aggressive.mighty_saurok"),
420            ],
421        },
422        Spot::MyrmidonTemple => SpotConfig {
423            base_structures: Some("spots.myrmidon-temple"),
424            entity_radius: 10.0,
425            entities: &[
426                (3..5, "common.entity.dungeon.myrmidon.hoplite"),
427                (3..5, "common.entity.dungeon.myrmidon.strategian"),
428                (2..3, "common.entity.dungeon.myrmidon.marksman"),
429            ],
430        },
431        Spot::WitchHouse => SpotConfig {
432            base_structures: Some("spots_general.witch_hut"),
433            entity_radius: 1.0,
434            entities: &[
435                (1..2, "common.entity.spot.witch_dark"),
436                (0..4, "common.entity.wild.peaceful.cat"),
437                (0..3, "common.entity.wild.peaceful.frog"),
438            ],
439        },
440        Spot::Igloo => SpotConfig {
441            base_structures: Some("spots_general.igloo"),
442            entity_radius: 2.0,
443            entities: &[
444                (3..5, "common.entity.dungeon.adlet.hunter"),
445                (3..5, "common.entity.dungeon.adlet.icepicker"),
446                (2..3, "common.entity.dungeon.adlet.tracker"),
447            ],
448        },
449        Spot::GnarlingTotem => SpotConfig {
450            base_structures: Some("site_structures.gnarling.totem"),
451            entity_radius: 30.0,
452            entities: &[
453                (3..5, "common.entity.dungeon.gnarling.mugger"),
454                (3..5, "common.entity.dungeon.gnarling.stalker"),
455                (3..5, "common.entity.dungeon.gnarling.logger"),
456                (2..4, "common.entity.dungeon.gnarling.mandragora"),
457                (1..3, "common.entity.wild.aggressive.deadwood"),
458                (1..2, "common.entity.dungeon.gnarling.woodgolem"),
459            ],
460        },
461        Spot::FallenTree => SpotConfig {
462            base_structures: Some("spots_grasslands.fallen_tree"),
463            entity_radius: 64.0,
464            entities: &[
465                (1..2, "common.entity.dungeon.gnarling.mandragora"),
466                (2..6, "common.entity.wild.aggressive.deadwood"),
467                (0..2, "common.entity.wild.aggressive.mossdrake"),
468            ],
469        },
470        // Random World Objects
471        Spot::LionRock => SpotConfig {
472            base_structures: Some("spots_savannah.lion_rock"),
473            entity_radius: 30.0,
474            entities: &[
475                (5..10, "common.entity.spot.female_lion"),
476                (1..2, "common.entity.wild.aggressive.male_lion"),
477            ],
478        },
479        Spot::WolfBurrow => SpotConfig {
480            base_structures: Some("spots_savannah.wolf_burrow"),
481            entity_radius: 10.0,
482            entities: &[(5..8, "common.entity.wild.aggressive.wolf")],
483        },
484        Spot::TreeStumpForest => SpotConfig {
485            base_structures: Some("trees.oak_stumps"),
486            entity_radius: 30.0,
487            entities: &[(0..2, "common.entity.wild.aggressive.deadwood")],
488        },
489        Spot::DesertBones => SpotConfig {
490            base_structures: Some("spots.bones"),
491            entity_radius: 40.0,
492            entities: &[(4..9, "common.entity.wild.aggressive.hyena")],
493        },
494        Spot::Arch => SpotConfig {
495            base_structures: Some("spots.arch"),
496            entity_radius: 50.0,
497            entities: &[],
498        },
499        Spot::AirshipCrash => SpotConfig {
500            base_structures: Some("trees.airship_crash"),
501            entity_radius: 20.0,
502            entities: &[(4..9, "common.entity.spot.grim_salvager")],
503        },
504        Spot::FruitTree => SpotConfig {
505            base_structures: Some("trees.fruit_trees"),
506            entity_radius: 2.0,
507            entities: &[(0..2, "common.entity.spot.bear")],
508        },
509        Spot::GnomeSpring => SpotConfig {
510            base_structures: Some("spots.gnome_spring"),
511            entity_radius: 40.0,
512            entities: &[(7..10, "common.entity.spot.gnome.spear")],
513        },
514        Spot::Shipwreck => SpotConfig {
515            base_structures: Some("spots.water.shipwreck"),
516            entity_radius: 2.0,
517            entities: &[(0..2, "common.entity.wild.peaceful.clownfish")],
518        },
519        Spot::Shipwreck2 => SpotConfig {
520            base_structures: Some("spots.water.shipwreck2"),
521            entity_radius: 20.0,
522            entities: &[(0..3, "common.entity.wild.peaceful.clownfish")],
523        },
524        Spot::GraveSmall => SpotConfig {
525            base_structures: Some("spots.grave_small"),
526            entity_radius: 2.0,
527            entities: &[],
528        },
529        Spot::RonFile(properties) => SpotConfig {
530            base_structures: Some(&properties.base_structures),
531            entity_radius: 1.0,
532            entities: &[],
533        },
534    }
535}
536
537pub fn apply_spots_to(canvas: &mut Canvas, _dynamic_rng: &mut impl Rng) {
538    let nearby_spots = canvas.nearby_spots().collect::<Vec<_>>();
539
540    for (spot_wpos2d, spot, seed) in nearby_spots.iter().copied() {
541        let mut rng = ChaChaRng::from_seed(seed_expan::rng_state(seed));
542
543        let units = UnitChooser::new(seed).get(seed).into();
544
545        let spot_config = spot_config(&spot);
546        // Blit base structure
547        if let Some(base_structures) = spot_config.base_structures {
548            let structures = Structure::load_group(base_structures).read();
549            let structure = structures.choose(&mut rng).unwrap();
550            let origin = spot_wpos2d.with_z(
551                canvas
552                    .col_or_gen(spot_wpos2d)
553                    .map(|c| c.alt as i32)
554                    .unwrap_or(0),
555            );
556            canvas.blit_structure(origin, structure, seed, units, true);
557        }
558
559        // Spawn entities
560        const PHI: f32 = 1.618;
561        for (spawn_count, spec) in spot_config.entities {
562            let spawn_count = rng.gen_range(spawn_count.clone()).max(0);
563
564            let dir_offset = rng.gen::<f32>();
565            for i in 0..spawn_count {
566                let dir = Vec2::new(
567                    ((dir_offset + i as f32 * PHI) * std::f32::consts::TAU).sin(),
568                    ((dir_offset + i as f32 * PHI) * std::f32::consts::TAU).cos(),
569                );
570                let dist = i as f32 / spawn_count as f32 * spot_config.entity_radius;
571                let wpos2d = spot_wpos2d + (dir * dist).map(|e| e.round() as i32);
572
573                let alt = canvas.col_or_gen(wpos2d).map(|c| c.alt as i32).unwrap_or(0);
574
575                if let Some(wpos) = canvas
576                    .area()
577                    .contains_point(wpos2d)
578                    .then(|| canvas.find_spawn_pos(wpos2d.with_z(alt)))
579                    .flatten()
580                {
581                    canvas.spawn(
582                        EntityInfo::at(wpos.map(|e| e as f32) + Vec3::new(0.5, 0.5, 0.0))
583                            .with_asset_expect(spec, &mut rng, None),
584                    );
585                }
586            }
587        }
588    }
589}
590
591pub fn is_valid(condition: &SpotCondition, g: f32, c: &SimChunk) -> bool {
592    c.sites.is_empty()
593        && match condition {
594            SpotCondition::MaxGradient(value) => g < *value,
595            SpotCondition::Biome(biomes) => biomes.contains(&c.get_biome()),
596            SpotCondition::NearCliffs => c.near_cliffs(),
597            SpotCondition::NearRiver => c.river.near_water(),
598            SpotCondition::IsWay => c.path.0.is_way(),
599            SpotCondition::IsUnderwater => c.is_underwater(),
600            SpotCondition::Typical => {
601                !c.near_cliffs() && !c.river.near_water() && !c.path.0.is_way()
602            },
603            SpotCondition::MinWaterDepth(depth) => {
604                is_valid(&SpotCondition::IsUnderwater, g, c) && c.water_alt > c.alt + depth
605            },
606            SpotCondition::Not(condition) => !is_valid(condition, g, c),
607            SpotCondition::All(conditions) => conditions.iter().all(|cond| is_valid(cond, g, c)),
608            SpotCondition::Any(conditions) => conditions.iter().any(|cond| is_valid(cond, g, c)),
609        }
610}
611
612#[test]
613fn test_spot_configs() {
614    let all_spots = [
615        Spot::DwarvenGrave,
616        Spot::SaurokAltar,
617        Spot::MyrmidonTemple,
618        Spot::GnarlingTotem,
619        Spot::WitchHouse,
620        Spot::GnomeSpring,
621        Spot::WolfBurrow,
622        Spot::Igloo,
623        Spot::LionRock,
624        Spot::TreeStumpForest,
625        Spot::DesertBones,
626        Spot::Arch,
627        Spot::AirshipCrash,
628        Spot::FruitTree,
629        Spot::Shipwreck,
630        Spot::Shipwreck2,
631        Spot::FallenTree,
632        Spot::GraveSmall,
633        Spot::JungleTemple,
634        Spot::SaurokTotem,
635        Spot::JungleOutpost,
636    ];
637
638    for spot in all_spots
639        .into_iter()
640        .chain(RON_SPOT_PROPERTIES.0.iter().map(Spot::RonFile))
641    {
642        let config = spot_config(&spot);
643
644        if let Some(base_structures) = config.base_structures {
645            let _structures = Structure::load_group(base_structures).read();
646        }
647        // Entities are tested in another test so don't really need to test it
648        // here.
649    }
650}