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