fish_lib/game/systems/
encounter_system.rs

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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
use crate::get_config;
use chrono::{DateTime, Timelike};
use chrono_tz::Tz;
use rand::seq::SliceRandom;
use rand::Rng;
use std::collections::HashMap;

pub type SpeciesId = i32;
pub type LocationId = i32;
pub type RarityLevel = u8;

pub type RarityEncounters = HashMap<RarityLevel, Vec<SpeciesId>>;
pub type LocationEncounters = HashMap<LocationId, RarityEncounters>;
pub type WeatherEncounters = HashMap<EncounterWeather, LocationEncounters>;
pub type HourlyEncounters = HashMap<u8, WeatherEncounters>;

#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)]
pub enum EncounterWeather {
    Any,
    Rain,
}

pub struct EncounterSystem {
    /// Hour -> Weather -> Location ID -> (Species ID, Rarity Level)
    encounters: HourlyEncounters,
    cached_weights: HashMap<RarityLevel, u64>,
}

impl Default for EncounterSystem {
    fn default() -> Self {
        Self::new()
    }
}

impl EncounterSystem {
    pub fn new() -> Self {
        let mut encounters: HourlyEncounters = HashMap::new();

        let config = get_config();
        for (species_id, species_data) in &config.species {
            for encounter in &species_data.encounters {
                let weather = if encounter.needs_rain {
                    EncounterWeather::Rain
                } else {
                    EncounterWeather::Any
                };

                for hour in encounter.get_hours() {
                    encounters
                        .entry(hour)
                        .or_default()
                        .entry(weather)
                        .or_default()
                        .entry(encounter.location_id)
                        .or_default()
                        .entry(encounter.rarity_level)
                        .or_default()
                        .push(*species_id);
                }
            }
        }

        let rarity_exponent = get_config().settings.rarity_exponent;
        let cached_weights = (0..=255)
            .map(|level| (level, Self::rarity_level_weight(level, rarity_exponent)))
            .collect();

        Self {
            encounters,
            cached_weights,
        }
    }

    fn rarity_level_weight(rarity_level: RarityLevel, rarity_exponent: f64) -> u64 {
        ((255 - rarity_level) as f64).powf(rarity_exponent) as u64 + 1
    }

    fn roll_rarity_level(&self, available_rarities: &[RarityLevel]) -> Option<RarityLevel> {
        if available_rarities.is_empty() {
            return None;
        }

        let cumulative_weights: Vec<u64> = available_rarities
            .iter()
            .scan(0u64, |sum, &rarity| {
                *sum += self.cached_weights[&rarity];
                Some(*sum)
            })
            .collect();

        let total = cumulative_weights.last()?;

        let mut rng = rand::thread_rng();
        let roll = rng.gen_range(0..*total);

        let index = cumulative_weights.partition_point(|&weight| weight <= roll);
        Some(available_rarities[index])
    }

    fn get_possible_rarity_encounters(
        &self,
        time: DateTime<Tz>,
        weather: EncounterWeather,
        location_id: i32,
    ) -> Option<&RarityEncounters> {
        self.encounters
            .get(&(time.hour() as u8))?
            .get(&weather)?
            .get(&location_id)
    }

    pub fn roll_encounter(
        &self,
        time: DateTime<Tz>,
        weather: EncounterWeather,
        location_id: LocationId,
    ) -> Option<SpeciesId> {
        let possible_rarity_encounters =
            self.get_possible_rarity_encounters(time, weather, location_id)?;

        let valid_rarity_levels: Vec<RarityLevel> =
            possible_rarity_encounters.keys().copied().collect();
        let rarity = self.roll_rarity_level(&valid_rarity_levels)?;

        let mut rng = rand::thread_rng();
        let possible_species = possible_rarity_encounters.get(&rarity)?;
        possible_species.choose(&mut rng).copied()
    }
}