fish_lib/game/systems/
encounter_system.rs

1use crate::data::species_data::SpeciesData;
2use chrono::{DateTime, Timelike};
3use chrono_tz::Tz;
4use rand::seq::IndexedRandom;
5use rand::Rng;
6use std::collections::HashMap;
7use std::sync::Arc;
8
9pub type SpeciesId = i32;
10pub type LocationId = i32;
11pub type RarityLevel = u8;
12
13pub type RarityEncounters = HashMap<RarityLevel, Vec<SpeciesId>>;
14pub type LocationEncounters = HashMap<LocationId, RarityEncounters>;
15pub type WeatherEncounters = HashMap<EncounterWeather, LocationEncounters>;
16pub type HourlyEncounters = HashMap<u8, WeatherEncounters>;
17
18#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)]
19pub enum EncounterWeather {
20    Any,
21    Rain,
22}
23
24pub struct EncounterSystem {
25    /// Hour -> Weather -> Location ID -> (Species ID, Rarity Level)
26    encounters: HourlyEncounters,
27    cached_weights: HashMap<RarityLevel, u64>,
28}
29
30impl EncounterSystem {
31    pub fn new(species: Arc<HashMap<i32, Arc<SpeciesData>>>, rarity_exponent: f64) -> Self {
32        let mut encounters: HourlyEncounters = HashMap::new();
33
34        for (species_id, species_data) in species.iter() {
35            for encounter in &species_data.encounters {
36                let weather = if encounter.needs_rain {
37                    EncounterWeather::Rain
38                } else {
39                    EncounterWeather::Any
40                };
41
42                for hour in encounter.get_hours() {
43                    encounters
44                        .entry(hour)
45                        .or_default()
46                        .entry(weather)
47                        .or_default()
48                        .entry(encounter.location_id)
49                        .or_default()
50                        .entry(encounter.rarity_level)
51                        .or_default()
52                        .push(*species_id);
53                }
54            }
55        }
56
57        let cached_weights = (0..=255)
58            .map(|level| (level, Self::rarity_level_weight(level, rarity_exponent)))
59            .collect();
60
61        Self {
62            encounters,
63            cached_weights,
64        }
65    }
66
67    fn rarity_level_weight(rarity_level: RarityLevel, rarity_exponent: f64) -> u64 {
68        ((255 - rarity_level) as f64).powf(rarity_exponent) as u64 + 1
69    }
70
71    fn roll_rarity_level(&self, available_rarities: &[RarityLevel]) -> Option<RarityLevel> {
72        if available_rarities.is_empty() {
73            return None;
74        }
75
76        let cumulative_weights: Vec<u64> = available_rarities
77            .iter()
78            .scan(0u64, |sum, &rarity| {
79                *sum += self.cached_weights[&rarity];
80                Some(*sum)
81            })
82            .collect();
83
84        let total = cumulative_weights.last()?;
85
86        let mut rng = rand::rng();
87        let roll = rng.random_range(0..*total);
88
89        let index = cumulative_weights.partition_point(|&weight| weight <= roll);
90        Some(available_rarities[index])
91    }
92
93    fn get_possible_rarity_encounters(
94        &self,
95        time: DateTime<Tz>,
96        weather: EncounterWeather,
97        location_id: i32,
98    ) -> Option<&RarityEncounters> {
99        self.encounters
100            .get(&(time.hour() as u8))?
101            .get(&weather)?
102            .get(&location_id)
103    }
104
105    pub fn roll_encounter(
106        &self,
107        time: DateTime<Tz>,
108        weather: EncounterWeather,
109        location_id: LocationId,
110    ) -> Option<SpeciesId> {
111        let possible_rarity_encounters =
112            self.get_possible_rarity_encounters(time, weather, location_id)?;
113
114        let valid_rarity_levels: Vec<RarityLevel> =
115            possible_rarity_encounters.keys().copied().collect();
116        let rarity = self.roll_rarity_level(&valid_rarity_levels)?;
117
118        let mut rng = rand::rng();
119        let possible_species = possible_rarity_encounters.get(&rarity)?;
120        possible_species.choose(&mut rng).copied()
121    }
122}