Skip to main content

thedes_session/
lib.rs

1use camera::Camera;
2use num::rational::Ratio;
3use rand::{SeedableRng, distr::Distribution, rngs::StdRng};
4use thedes_asset::Assets;
5use thedes_dev::CommandContext;
6use thedes_domain::{
7    event::{self, MetaEvent},
8    game::{Game, MovePlayerError},
9    stat::StatValue,
10};
11use thedes_gen::event::{self as gen_event};
12use thedes_geometry::orientation::Direction;
13use thedes_settings::AudioSinkType;
14use thedes_tui::{
15    core::{
16        App,
17        audio::{self, PlayOptions, Volume},
18        color::{BasicColor, ColorPair},
19        geometry::{Coord, CoordPair},
20    },
21    text,
22};
23
24use thiserror::Error;
25
26use crate::camera::DynamicStyle;
27
28pub mod camera;
29
30#[derive(Debug, Error)]
31pub enum RenderError {
32    #[error("Failed to handle session camera")]
33    Camera(
34        #[from]
35        #[source]
36        camera::Error,
37    ),
38    #[error("Failed to write HP hearts")]
39    HpHearts(#[source] text::Error),
40    #[error("Failed to write HP text")]
41    HpText(#[source] text::Error),
42}
43
44#[derive(Debug, Error)]
45pub enum EventError {
46    #[error("Failed to create event distribution")]
47    Distr(
48        #[from]
49        #[source]
50        gen_event::DistrError,
51    ),
52    #[error("Failed to apply event to game")]
53    Apply(
54        #[from]
55        #[source]
56        event::ApplyError,
57    ),
58}
59
60#[derive(Debug, Error)]
61pub enum MetaEventError {
62    #[error("Failed to flush audio commands")]
63    FlushAudio(#[from] audio::FlushError),
64}
65
66#[derive(Debug, Error)]
67pub enum MoveAroundError {
68    #[error("Failed to move player pointer")]
69    MovePlayer(
70        #[from]
71        #[source]
72        MovePlayerError,
73    ),
74}
75
76#[derive(Debug, Error)]
77pub enum QuickStepError {
78    #[error("Failed to move player head")]
79    MovePlayer(
80        #[from]
81        #[source]
82        MovePlayerError,
83    ),
84}
85
86#[derive(Debug, Clone)]
87pub struct Config {
88    camera: camera::Config,
89    event_interval: Ratio<u64>,
90    event_tick_size: u64,
91    event_distr_config: gen_event::DistrConfig,
92}
93
94impl Default for Config {
95    fn default() -> Self {
96        Self::new()
97    }
98}
99
100impl Config {
101    pub fn new() -> Self {
102        Self {
103            camera: camera::Config::new(),
104            event_interval: Ratio::new(4, 100),
105            event_tick_size: 2,
106            event_distr_config: gen_event::DistrConfig::new(),
107        }
108    }
109
110    pub fn with_camera(self, config: camera::Config) -> Self {
111        Self { camera: config, ..self }
112    }
113
114    pub fn with_event_interval(self, ticks: Ratio<u64>) -> Self {
115        Self { event_interval: ticks, ..self }
116    }
117
118    pub fn with_event_tick_size(self, size: u64) -> Self {
119        Self { event_tick_size: size, ..self }
120    }
121
122    pub fn with_event_distr(self, config: gen_event::DistrConfig) -> Self {
123        Self { event_distr_config: config, ..self }
124    }
125
126    pub fn finish(self, game: Game) -> Session {
127        Session {
128            rng: StdRng::from_os_rng(),
129            game,
130            camera: self.camera.finish(),
131            event_interval: self.event_interval,
132            event_ticks: Ratio::ZERO,
133            event_tick_size: self.event_tick_size,
134            event_distr_config: self.event_distr_config,
135        }
136    }
137}
138
139#[derive(Debug, Clone)]
140pub struct Session {
141    rng: StdRng,
142    game: Game,
143    camera: Camera,
144    event_interval: Ratio<u64>,
145    event_ticks: Ratio<u64>,
146    event_tick_size: u64,
147    event_distr_config: gen_event::DistrConfig,
148}
149
150impl Session {
151    const STAT_VALUE_WIDTH: Coord = 3;
152    const GAME_INFO_WIDTH: Coord = Self::STAT_VALUE_WIDTH * 2 + 1;
153
154    const POS_HEIGHT: Coord = 1;
155
156    pub fn render(&mut self, app: &mut App) -> Result<(), RenderError> {
157        self.camera.render(
158            app,
159            &mut self.game,
160            &DynamicStyle {
161                margin_top_left: CoordPair { y: 1, x: Self::GAME_INFO_WIDTH },
162                margin_bottom_right: CoordPair { y: 0, x: 0 },
163            },
164        )?;
165        self.render_hp(app)?;
166        Ok(())
167    }
168
169    pub fn tick_event(&mut self) -> Result<(), EventError> {
170        self.event_ticks += self.event_tick_size;
171        while self.event_ticks >= self.event_interval {
172            self.event_ticks -= self.event_interval;
173            let event = self
174                .event_distr_config
175                .finish(&self.game)?
176                .sample(&mut self.rng);
177            self.game.schedule_event(event, 0);
178            self.game.execute_events()?;
179        }
180        Ok(())
181    }
182
183    pub fn consume_meta_events(
184        &mut self,
185        app: &mut App,
186        assets: &'static Assets,
187    ) -> Result<(), MetaEventError> {
188        while let Some(meta_event) = self.game_mut().read_one_meta_event() {
189            match meta_event {
190                MetaEvent::MonsterHit(pos) => {
191                    self.consume_monster_hit(pos, app, assets)
192                },
193                MetaEvent::MonsterGrowl(pos) => {
194                    self.consume_monster_growl(pos, app, assets)
195                },
196            }
197        }
198
199        app.audio_controller.flush()?;
200
201        Ok(())
202    }
203
204    fn consume_monster_hit(
205        &self,
206        pos: CoordPair,
207        app: &mut App,
208        assets: &'static Assets,
209    ) {
210        if !self.game().player().position().contains(pos) {
211            return;
212        }
213        app.audio_controller.queue([audio::Command::new_play_once(
214            AudioSinkType::Fx,
215            &assets.sound.hit[..],
216        )]);
217    }
218
219    fn consume_monster_growl(
220        &self,
221        pos: CoordPair,
222        app: &mut App,
223        assets: &'static Assets,
224    ) {
225        if !self.camera.contains(pos) {
226            return;
227        }
228
229        let distances = self
230            .game()
231            .player()
232            .position()
233            .head()
234            .zip2_with(pos, Coord::abs_diff);
235        let distance = (distances.y + distances.x) as u64;
236        let distance_total = self.camera.half_view_perimeter() as u64;
237        let volume_max = Volume::MAX as u64;
238        let relative_volume =
239            (distance * volume_max / distance_total) as Volume;
240
241        app.audio_controller.queue([audio::Command::new_play_once_with(
242            AudioSinkType::Fx,
243            &assets.sound.growl[..],
244            PlayOptions { relative_volume },
245        )]);
246    }
247
248    pub fn move_around(
249        &mut self,
250        direction: Direction,
251    ) -> Result<(), MoveAroundError> {
252        self.game.move_player_pointer(direction)?;
253        Ok(())
254    }
255
256    pub fn quick_step(
257        &mut self,
258        direction: Direction,
259    ) -> Result<(), QuickStepError> {
260        self.game.move_player_head(direction)?;
261        Ok(())
262    }
263
264    pub fn game(&self) -> &Game {
265        &self.game
266    }
267
268    pub fn game_mut(&mut self) -> &mut Game {
269        &mut self.game
270    }
271
272    pub fn dev_command_context<'a>(&'a mut self) -> CommandContext<'a, 'a> {
273        CommandContext {
274            game: &mut self.game,
275            event_distr_config: &mut self.event_distr_config,
276        }
277    }
278
279    fn render_hp(&self, app: &mut App) -> Result<(), RenderError> {
280        let player_hp = self.game.player().hp();
281        let width = StatValue::from(Self::GAME_INFO_WIDTH);
282        let compensated_hearts =
283            player_hp.value() * width + player_hp.curr_max() - 1;
284        let heart_count = compensated_hearts / player_hp.curr_max();
285        let heart_count = heart_count as usize;
286        let empty_heart_count = width as usize - heart_count;
287        let hearts = "♥︎".repeat(heart_count) + &"♡".repeat(empty_heart_count);
288
289        let hearts_point = CoordPair { y: Self::POS_HEIGHT, x: 0 };
290        let hearts_colors = ColorPair {
291            background: BasicColor::Black.into(),
292            foreground: BasicColor::LightRed.into(),
293        };
294        text::inline(app, hearts_point, &hearts, hearts_colors)
295            .map_err(RenderError::HpHearts)?;
296
297        let numbers = format!(
298            "{:>w$}/{:<w$}",
299            player_hp.value(),
300            player_hp.curr_max(),
301            w = usize::from(Self::STAT_VALUE_WIDTH),
302        );
303        let hp_point = CoordPair { y: Self::POS_HEIGHT + 1, x: 0 };
304        let hp_colors = ColorPair {
305            background: BasicColor::Black.into(),
306            foreground: BasicColor::White.into(),
307        };
308        text::inline(app, hp_point, &numbers, hp_colors)
309            .map_err(RenderError::HpText)?;
310
311        Ok(())
312    }
313}