Skip to main content

hvz/
zombie.rs

1use crate::establish_connection;
2use crate::interactions::Interaction;
3use crate::player::Player;
4use crate::player::PlayerState;
5use crate::schema::players;
6use crate::time::CustomDateTime;
7use chrono::Datelike;
8use chrono::NaiveDateTime;
9use chrono::Utc;
10use diesel::ExpressionMethods;
11use diesel::QueryDsl;
12use diesel::RunQueryDsl;
13use log::{info, trace};
14use std::time::Duration;
15use tokio::time::interval;
16
17/// Represents a zombie player.
18pub type Zombie = Player;
19
20impl Zombie {
21    /// Calculates the next feeding time for a zombie player.
22    ///
23    /// Returns `None` if the player is not a zombie.
24    ///
25    /// # Errors
26    /// Returns an error if the player is not a zombie.
27    pub async fn get_next_feeding(&self) -> Result<Option<CustomDateTime>, String> {
28        Ok(self.next_feeding)
29    }
30
31    /// Gets the starvation status of all the zombies.
32    ///
33    /// # Returns
34    /// A vector of tuples containing the player's alias and their next feeding time.
35    ///
36    /// # Errors
37    /// Returns an error if the player is not a zombie.
38    pub async fn starvation(&self) -> Result<Vec<(String, Option<CustomDateTime>)>, String> {
39        let connection = &mut establish_connection();
40
41        let starvation_all = players::table
42            .select((players::alias, players::state, players::next_feeding))
43            .filter(players::state.eq(PlayerState::Zombie))
44            .or_filter(players::state.eq(PlayerState::Zombie0))
45            .order(players::next_feeding.asc())
46            .load::<(String, PlayerState, Option<CustomDateTime>)>(connection)
47            .map_err(|e| format!("Failed to load starvation: {}", e))?;
48
49        let mut seen_z0 = false;
50        let mut starvation = Vec::new();
51        for (alias, state, next_feeding) in starvation_all {
52            if state == PlayerState::Zombie0 {
53                if !seen_z0 {
54                    starvation.push(("Z0".to_owned(), next_feeding));
55                    seen_z0 = true;
56                }
57            } else {
58                starvation.push((alias, next_feeding));
59            }
60        }
61
62        Ok(starvation)
63    }
64
65    /// Hunt a player and feed two other players.
66    ///
67    /// # Arguments
68    /// * `target` - The player to hunt.
69    /// * `feed1` - The first player to feed.
70    /// * `feed2` - The second player to feed.
71    ///
72    /// # Errors
73    /// Returns an error if the player is not a zombie.
74    pub async fn hunt(
75        &self,
76        target: &Player,
77        feed1: Option<&Player>,
78        feed2: Option<&Player>,
79    ) -> Result<(), String> {
80        target.zombify().await?;
81        self.feed(None).await?;
82        target.feed(None).await?;
83        if let Some(zombie_feed1) = feed1 {
84            zombie_feed1.feed(None).await?;
85        }
86        if let Some(zombie_feed2) = feed2 {
87            zombie_feed2.feed(None).await?;
88        }
89
90        Interaction::save_hunt_interaction(self, target, feed1, feed2).await?;
91
92        Ok(())
93    }
94
95    /// Zombify a player
96    ///
97    /// # Arguments
98    /// * `self` - The player to zombify.
99    ///
100    /// # Errors
101    /// Returns an error if the player is not a zombie.
102    pub async fn zombify(&self) -> Result<(), String> {
103        let connection = &mut establish_connection();
104
105        diesel::update(players::table.filter(players::id.eq(self.id.clone())))
106            .set(players::state.eq(PlayerState::Zombie))
107            .execute(connection)
108            .expect("Failed to zombify player.");
109
110        info!("Player {:?} hunted", self.name);
111
112        Ok(())
113    }
114
115    /// Feed a player.
116    ///
117    /// # Arguments
118    /// * `amount` - The amount of time to feed the player in hours.
119    ///
120    /// # Errors
121    /// Returns an error if the player is not a zombie.
122    pub async fn feed(&self, amount: Option<i64>) -> Result<Option<CustomDateTime>, String> {
123        let connection = &mut establish_connection();
124
125        let mut new_feeding_time: CustomDateTime = Self::feeding_time_calc()?;
126        if let Some(amount) = amount {
127            new_feeding_time = CustomDateTime::now() + chrono::Duration::hours(amount);
128            let weekday = new_feeding_time.weekday().num_days_from_monday();
129            if weekday == 5 || weekday == 6 {
130                new_feeding_time += chrono::Duration::days(2);
131            }
132        }
133
134        diesel::update(players::table.filter(players::id.eq(self.id.clone())))
135            .set(players::next_feeding.eq(new_feeding_time))
136            .execute(connection)
137            .expect("Failed to update player next feeding");
138        return Ok(Some(new_feeding_time));
139    }
140
141    /// Calculates the next feeding time for a zombie.
142    ///
143    /// # Returns
144    /// The next feeding time as a NaiveDateTime.
145    pub fn feeding_time_calc() -> Result<CustomDateTime, String> {
146        let day_of_the_week = chrono::Local::now().weekday().num_days_from_monday();
147
148        let next_feeding_time = match day_of_the_week {
149            0 | 1 => CustomDateTime::now() + chrono::Duration::days(3),
150            2 | 3 | 4 => CustomDateTime::now() + chrono::Duration::days(5),
151            5 => CustomDateTime::now() + chrono::Duration::days(4),
152            6 => CustomDateTime::now() + chrono::Duration::days(3),
153            _ => return Err("Invalid day of the week".to_string()),
154        };
155
156        Ok(next_feeding_time)
157    }
158
159    /// Monitors the zombie death and updates the player state accordingly.
160    ///
161    /// Every minute, it checks if the player's next feeding time has passed.
162    /// If so, it updates the player's state to Corpse and sets the next feeding time to None.
163    ///
164    /// # Errors
165    /// Returns an error if the player is not a zombie.
166    pub async fn monitor_zombie_death() -> ! {
167        let connection = &mut establish_connection();
168        let mut interval = interval(Duration::from_secs(60)); // 1 minute
169        let date_none: Option<NaiveDateTime> = None;
170        loop {
171            interval.tick().await;
172            trace!("Checking zombie death... Current time: {}", Utc::now());
173            diesel::update(players::table.filter(players::next_feeding.lt(CustomDateTime::now())))
174                .set((
175                    players::state.eq(PlayerState::Corpse),
176                    players::next_feeding.eq(date_none),
177                ))
178                .execute(connection)
179                .expect("Failed to update player state");
180        }
181    }
182}