Skip to main content

rsnaker/graphics/sprites/
fruit.rs

1//! # Fruit Management Module
2//!
3//! This module defines the `Fruit` struct, which represents different fruits in the game logic and provides the
4//!  ability to create, position, and render them.
5//!
6//! The `FRUITS_SCORES_PROBABILITIES` constant defines various fruits with their respective scores and spawn probabilities.
7//!
8//! # Example
9//! ```rust
10//! use rsnaker::graphics::graphic_block::Position;
11//! use rsnaker::graphics::sprites::fruit::Fruit;
12//! use std::time::Duration;
13//!
14//! let position = Position { x: 5, y: 10 };
15//! let apple = Fruit::new(40, 2, position, "🍎", 5f32, true);
16//! assert_eq!(apple.get_score(), 40);
17//! ```
18
19use crate::graphics::graphic_block::{GraphicBlock, Position};
20use ratatui::buffer::Buffer;
21use ratatui::layout::Rect;
22use ratatui::prelude::Widget;
23use ratatui::style::Style;
24use ratatui::widgets::Paragraph;
25use ratatui::widgets::WidgetRef;
26use std::time::{Duration, Instant};
27
28/// Distribution statistics with weighted lottery / pie chart parts.
29/// Image, score, probability, size effect
30/// Yet pear, or strawberry ("🍓", 60, 5, 15),
31/// In order: Symbol, score effect, probability, size effect
32pub const FRUITS_SCORES_PROBABILITIES: &[(&str, i32, u16, i16)] = &[
33    ("🦞", -50, 5, -150),
34    ("🥥", -10, 5, -40),
35    ("🍇", 5, 4, 0),
36    ("🍐", 10, 10, 5),
37    ("🥝", 20, 10, 8),
38    ("🍋", 30, 15, 10),
39    ("🍌", 40, 15, 15),
40    ("🍉", 50, 15, 15),
41    ("🍎", 75, 15, 15),
42    ("🍓", 100, 5, 20),
43    ("🍒", 200, 1, 25),
44];
45
46/// Represents a fruit on the map.
47/// Fruits have a score value and are displayed as graphical blocks.
48#[derive(PartialEq, Debug, Clone)]
49pub struct Fruit<'a> {
50    score: i32,
51    grow_snake: i16,
52    graphic_block: GraphicBlock<'a>,
53    spawned_at: Instant,
54    lifetime: Duration,
55    timer_enabled: bool,
56    timer_paused_since: Option<Instant>,
57    accumulated_paused: Duration,
58}
59
60impl<'a> Fruit<'a> {
61    /// Returns the lifetime multiplier applied on top of the base duration.
62    #[must_use]
63    pub fn duration_multiplier(size_effect: i16) -> f32 {
64        if size_effect >= 0 {
65            (1.0 + (f32::from(size_effect) / 50.0)).min(2.0)
66        } else {
67            (1.0 + (f32::from(size_effect) / 300.0)).max(0.5)
68        }
69    }
70
71    /// Computes the lifetime from the base duration and the fruit bonus/malus.
72    #[must_use]
73    pub fn duration_from_base(base: Duration, size_effect: i16) -> Duration {
74        Duration::from_secs_f32(base.as_secs_f32() * Self::duration_multiplier(size_effect))
75    }
76
77    /// Creates a new `Fruit` at a given position with an associated score and image.
78    #[must_use]
79    pub fn new(
80        score: i32,
81        grow_snake_by_relative_nb: i16,
82        position: Position,
83        image: &'a str,
84        base_lifetime: f32,
85        timer_enabled: bool,
86    ) -> Fruit<'a> {
87        Self {
88            score,
89            grow_snake: grow_snake_by_relative_nb,
90            graphic_block: GraphicBlock::new(position, image, Style::default()),
91            spawned_at: Instant::now(),
92            lifetime: Duration::from_secs_f32(
93                base_lifetime * Self::duration_multiplier(grow_snake_by_relative_nb),
94            ),
95            timer_enabled,
96            timer_paused_since: None,
97            accumulated_paused: Duration::ZERO,
98        }
99    }
100
101    /// Checks if the fruit is at a specific position.
102    #[must_use]
103    pub fn is_at_position(&self, position: &Position) -> bool {
104        self.graphic_block.get_position() == position
105    }
106
107    /// Checks if the fruit is at a specific position.
108    pub fn set_position(&mut self, position: Position) {
109        self.graphic_block.set_position(position);
110    }
111
112    /// Pauses or resumes the fruit timer.
113    pub fn set_timer_paused(&mut self, paused: bool, now: Instant) {
114        match (paused, self.timer_paused_since) {
115            (true, None) => {
116                // going to pause, keep the pause timing
117                // NB timer_paused_since is also a boolean for pause or not with Some/None
118                self.timer_paused_since = Some(now);
119            }
120            (false, Some(paused_since)) => {
121                // going to play again, add the pause timing to the elapsed time
122                self.accumulated_paused += now.duration_since(paused_since);
123                self.timer_paused_since = None;
124            }
125            _ => {}
126        }
127    }
128
129    /// Returns true if the fruit timer is enabled and the fruit lifetime is over.
130    #[must_use]
131    pub fn is_expired(&self, now: Instant) -> bool {
132        self.timer_enabled
133            && self.timer_paused_since.is_none()
134            && self.time_elasped_with_breaks_sub(now) >= self.lifetime
135    }
136
137    /// Returns the remaining time before the fruit expires.
138    #[must_use]
139    pub fn remaining_time(&self, now: Instant) -> Option<Duration> {
140        if self.timer_enabled {
141            self.lifetime
142                .checked_sub(self.time_elasped_with_breaks_sub(now))
143        } else {
144            None
145        }
146    }
147
148    /// Returns the score of the fruit.
149    #[must_use]
150    pub fn get_score(&self) -> i32 {
151        self.score
152    }
153
154    #[must_use]
155    pub fn get_grow_snake(&self) -> i16 {
156        self.grow_snake
157    }
158
159    fn time_elasped_with_breaks_sub(&self, now: Instant) -> Duration {
160        now.duration_since(self.spawned_at).saturating_sub(
161            self.accumulated_paused
162                + if let Some(pause_time) = self.timer_paused_since {
163                    // if we are currently paused, we need to add the pause time to the elapsed time
164                    // because it is savec in accumulated time only after going to play again
165                    now.duration_since(pause_time)
166                } else {
167                    Duration::ZERO
168                },
169        )
170    }
171}
172
173/// Enables `Fruit` to be rendered as a widget.
174impl Widget for Fruit<'_> {
175    fn render(self, area: Rect, buf: &mut Buffer) {
176        self.graphic_block.render(area, buf);
177    }
178}
179
180/// Enables `Fruit` to be rendered as a reference widget.
181impl WidgetRef for Fruit<'_> {
182    fn render_ref(&self, area: Rect, buf: &mut Buffer) {
183        let now = Instant::now();
184        if self.is_expired(now) {
185            return;
186        }
187
188        self.graphic_block.render_ref(area, buf);
189        if !self.timer_enabled {
190            return;
191        }
192
193        if let Some(remaining) = self.remaining_time(now) {
194            let timer_text = format!("{:.1}", remaining.as_secs_f32());
195            let position = self.graphic_block.get_position();
196            //The fruit manager let the right column free for displaying timing
197            // (ugly/strange to change timer position)
198            let timer_area = Rect::new(position.x.saturating_add(2), position.y, 4, 1);
199            Paragraph::new(timer_text).render(timer_area, buf);
200        }
201    }
202}