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}