fish_lib/models/
specimen.rs

1use crate::config::ConfigInterface;
2use crate::game::errors::resource::GameResourceError;
3use crate::game::errors::GameResult;
4use crate::traits::model::Model;
5use crate::utils::random::random_normal_01;
6use chrono::{DateTime, Utc};
7use diesel::{AsChangeset, Insertable, Queryable, Selectable};
8use rand::random;
9use serde::{Deserialize, Serialize};
10use std::sync::Arc;
11
12#[derive(
13    Debug, Default, Serialize, Deserialize, Clone, PartialEq, Queryable, Selectable, AsChangeset,
14)]
15#[diesel(table_name = crate::schema::fish_specimens)]
16#[diesel(check_for_backend(diesel::pg::Pg))]
17pub struct Specimen {
18    /// Primary key of this specimen in the database
19    pub id: i64,
20    /// The primary key of the user this specimen belongs to
21    pub user_id: i64,
22    /// The species ID this specimen is associated with (species are defined in the config)
23    pub species_id: i32,
24    /// When the dataset was created
25    pub created_at: DateTime<Utc>,
26    /// When the dataset was last updated
27    pub updated_at: DateTime<Utc>,
28    /// Baby size ratio (0-1) relative to species' min/max baby size range
29    pub size_baby_ratio: f32,
30    /// Adult size ratio (0-1) relative to species' min/max adult size range
31    pub size_adult_ratio: f32,
32    /// Lifespan ratio (0-1) relative to species' min/max lifespan range
33    pub lifespan_days_ratio: f32,
34    /// The age this fish was caught at (from 0 to 1)
35    pub catch_age: f32,
36}
37
38impl Specimen {
39    pub fn get_age(
40        &self,
41        config: Arc<dyn ConfigInterface>,
42        time_multiplier: f32,
43    ) -> GameResult<f32> {
44        let data = config
45            .get_species_data(self.species_id)
46            .ok_or_else(|| GameResourceError::species_not_found(self.species_id))?;
47        let lifespan_days = data.get_lifespan_days_by_ratio(self.lifespan_days_ratio);
48
49        if self.catch_age >= 1.0 {
50            return Ok(1.0);
51        }
52
53        let now = Utc::now();
54        let seconds_since_catch = (now - self.created_at).num_seconds();
55        let days_since_catch = seconds_since_catch as f32 / (86400f32);
56
57        let remaining_lifespan_days_after_catch = lifespan_days * (1.0 - self.catch_age);
58        let age_progress_since_catch =
59            (days_since_catch * time_multiplier) / remaining_lifespan_days_after_catch;
60
61        let age = (self.catch_age + age_progress_since_catch).clamp(0.0, 1.0);
62        Ok(age)
63    }
64
65    pub fn get_size_mm(
66        &self,
67        config: Arc<dyn ConfigInterface>,
68        time_multiplier: f32,
69    ) -> GameResult<f32> {
70        let data = config
71            .get_species_data(self.species_id)
72            .ok_or_else(|| GameResourceError::species_not_found(self.species_id))?;
73        let size_baby_mm = data.get_baby_size_by_ratio(self.size_baby_ratio);
74        let size_adult_mm = data.get_adult_size_by_ratio(self.size_adult_ratio);
75
76        let current_age = self.get_age(config, time_multiplier)?;
77        let size_mm = size_baby_mm + (size_adult_mm - size_baby_mm) * current_age;
78        Ok(size_mm)
79    }
80
81    pub fn get_weight_g(
82        &self,
83        config: Arc<dyn ConfigInterface>,
84        time_multiplier: f32,
85    ) -> GameResult<f32> {
86        let data = config
87            .get_species_data(self.species_id)
88            .ok_or_else(|| GameResourceError::species_not_found(self.species_id))?;
89        let weight_baby_g = data.get_baby_weight_by_ratio(self.size_baby_ratio);
90        let weight_adult_g = data.get_adult_weight_by_ratio(self.size_adult_ratio);
91
92        let current_age = self.get_age(config, time_multiplier)?;
93        let weight_g = weight_baby_g + (weight_adult_g - weight_baby_g) * current_age;
94        Ok(weight_g)
95    }
96
97    pub fn get_total_size_ratio(
98        &self,
99        config: Arc<dyn ConfigInterface>,
100        time_multiplier: f32,
101    ) -> GameResult<f32> {
102        let data = config
103            .get_species_data(self.species_id)
104            .ok_or_else(|| GameResourceError::species_not_found(self.species_id))?;
105        let min_possible_size = data.min_size_baby_mm as f32;
106        let max_possible_size = data.max_size_adult_mm as f32;
107
108        let current_size = self.get_size_mm(config, time_multiplier)?;
109
110        let ratio = (current_size - min_possible_size) / (max_possible_size - min_possible_size);
111        let total_size_ratio = ratio.clamp(0.0, 1.0);
112        Ok(total_size_ratio)
113    }
114}
115
116impl Model for Specimen {
117    type Table = crate::schema::fish_specimens::table;
118    type PrimaryKeyType = i64;
119    type InsertType = NewSpecimen;
120
121    fn table() -> Self::Table {
122        crate::schema::fish_specimens::table
123    }
124
125    fn id(&self) -> Self::PrimaryKeyType {
126        self.id
127    }
128}
129
130#[derive(Insertable)]
131#[diesel(table_name = crate::schema::fish_specimens)]
132#[diesel(check_for_backend(diesel::pg::Pg))]
133pub struct NewSpecimen {
134    pub user_id: i64,
135    pub species_id: i32,
136    pub size_baby_ratio: f32,
137    pub size_adult_ratio: f32,
138    pub lifespan_days_ratio: f32,
139    pub catch_age: f32,
140}
141
142impl NewSpecimen {
143    pub fn generate(user_id: i64, species_id: i32) -> NewSpecimen {
144        NewSpecimen {
145            user_id,
146            catch_age: random(),
147            species_id,
148            size_baby_ratio: random_normal_01(),
149            size_adult_ratio: random_normal_01(),
150            lifespan_days_ratio: random_normal_01(),
151        }
152    }
153}