fish_lib/game/systems/
encounter_system.rs1use 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 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}