1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
use bevy::prelude::*;
use noise::{NoiseFn, Perlin};
use rand::rngs::SmallRng;
use rand::{Rng, SeedableRng};

use crate::{enums::biome::Marker, state::Game};

use super::resources::{GeneratedMaps, GenerationSeed};

/// Boundary for the object id noise generation. This is set as both the upper and lower bounds,
/// with the lower bound being negative.
pub const OBJECT_BOUNDARY: f64 = 2.5;
/// Weight pool for object generation. We turn the noise into a usize that is within the range
/// of zero to this value.
///
/// It should be taken into account that some of the pool space should be used for empty space,
/// otherwise the map will be too cluttered.
pub const OBJECT_POOL: usize = 200;

/// Generate a new seed for the world generation.
///
/// This should be first in the sequence of events that happen when generating a new realm.
///
/// This system sets the `GenerationSeed` resource to a new random value.
pub(super) fn generate_new_seed(mut seed: ResMut<GenerationSeed>) {
    let rng = &mut rand::thread_rng();
    seed.0 = rng.gen();
    tracing::info!("New seed generated: {}", seed.0);
}

/// Progress the game state to the `Generating` state.
///
/// This is the last step in the sequence of events that happen when generating a new realm.
/// When that sequence is complete, the game state should change from `Game::Generating` to
/// `Game::Playing`.
pub(super) fn progress_to_playing(mut state: ResMut<NextState<Game>>) {
    state.set(Game::Playing);
    tracing::info!("Progressing to playing state");
}

/// Use the perlin noise generator to generate a biome map. And then use simplex noise to determine
/// how to place trees and other objects based on the biome map. Both should be seedable.
///
/// This system should be called when the game state is `Game::Generating`.
#[allow(clippy::needless_pass_by_value)]
pub(super) fn generate_map(seed: Res<GenerationSeed>, mut maps: ResMut<GeneratedMaps>) {
    tracing::info!("Generating map with seed: {}", seed.0);

    let perlin = Perlin::new(seed.0);
    let mut small_rng = SmallRng::seed_from_u64(seed.as_u64());

    let width = maps.dimensions().0;
    let height = maps.dimensions().1;

    maps.reset();

    for x in 0..width {
        for y in 0..height {
            // We allow precision loss here because it's a very edge case where it would matter.
            #[allow(clippy::cast_precision_loss)]
            let pos = [x as f64 / 100.0, y as f64 / 100.0, 0.0];
            let value = perlin.get(pos);
            let biome = Marker::from_noise(value);
            maps.biome_map[x].push(biome);

            // Map the small_rng noise for position to the object map.
            let value = small_rng.gen_range(-OBJECT_BOUNDARY..OBJECT_BOUNDARY);
            let object = noise_to_object(value);
            maps.object_map[x].push(object);
        }
    }

    tracing::info!(
        "Generated {}x{} map",
        maps.biome_map.len(),
        maps.biome_map[0].len()
    );
}

/// Take noise and return a usize between 0 and `OBJECT_POOL`.
///
/// # Arguments
///
/// * `noise` - The noise value to convert to an object marker. This is expected to be
/// between -`OBJECT_BOUNDARY` and `OBJECT_BOUNDARY`. Anything outside of this range will
/// be clamped to the nearest boundary.
///
/// # Returns
///
/// A usize between 0 and `OBJECT_POOL`.
#[must_use]
fn noise_to_object(noise: f64) -> usize {
    let noise = noise.clamp(-OBJECT_BOUNDARY, OBJECT_BOUNDARY);
    let noise = (noise + OBJECT_BOUNDARY) / (OBJECT_BOUNDARY * 2.0);
    #[allow(clippy::cast_sign_loss)]
    let object = (noise * OBJECT_POOL as f64) as usize;

    object
}