diff --git a/.gitignore b/.gitignore index 99d1388..076443d 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ Cargo.lock **/*.rs.bk /*.gif +/savegame.json diff --git a/game/Cargo.toml b/game/Cargo.toml index 4538ee0..e1da054 100644 --- a/game/Cargo.toml +++ b/game/Cargo.toml @@ -14,7 +14,7 @@ tracing = { version = "0.1", features = ["log"] } serde = { version = "1.0.126", features = ["derive"] } serde_json = "1.0.64" thiserror = "1.0" -chrono = "0.4" +chrono = { version = "0.4", features = ["serde"] } rust-embed = "6.2.0" raylib = { version = "3.5", git = "https://github.com/ewpratten/raylib-rs", rev = "2ae949cb3488dd1bb052ece71d61021c8dd6e910", features = [ "serde" @@ -31,7 +31,7 @@ pkg-version = "1.0" cfg-if = "1.0" num-derive = "0.3" num = "0.4" -tiled = { version ="0.9.5", default-features = false } +tiled = { version = "0.9.5", default-features = false } async-trait = "0.1.51" webbrowser = "0.5" diff --git a/game/src/context.rs b/game/src/context.rs index 10957df..b08f8e8 100644 --- a/game/src/context.rs +++ b/game/src/context.rs @@ -1,21 +1,24 @@ use std::{cell::RefCell, sync::mpsc::Sender}; -use chrono::{DateTime, Utc}; +use chrono::{DateTime, Duration, Utc}; use discord_sdk::activity::ActivityBuilder; -use crate::{utilities::non_ref_raylib::HackedRaylibHandle, GameConfig}; +use crate::{progress::ProgressData, utilities::non_ref_raylib::HackedRaylibHandle, GameConfig}; #[derive(Debug)] pub enum ControlFlag { Quit, SwitchLevel(usize), - UpdateLevelStart(DateTime) + UpdateLevelStart(DateTime), + SaveProgress, + MaybeUpdateHighScore(usize, Duration) } #[derive(Debug)] pub struct GameContext { pub renderer: RefCell, pub config: GameConfig, + pub player_progress: ProgressData, pub current_level: usize, pub level_start_time: DateTime, pub discord_rpc_send: Sender>, diff --git a/game/src/lib.rs b/game/src/lib.rs index 3b4c965..31aee85 100644 --- a/game/src/lib.rs +++ b/game/src/lib.rs @@ -81,6 +81,7 @@ use utilities::discord::DiscordConfig; use crate::{ context::GameContext, discord_rpc::{maybe_set_discord_presence, try_connect_to_local_discord}, + progress::ProgressData, scenes::{build_screen_state_machine, Scenes}, utilities::{ game_config::FinalShaderConfig, @@ -108,6 +109,7 @@ mod scenes; mod utilities; pub use utilities::{datastore::StaticGameData, game_config::GameConfig}; mod character; +mod progress; /// The game entrypoint pub async fn game_begin(game_config: &mut GameConfig) -> Result<(), Box> { @@ -148,6 +150,9 @@ pub async fn game_begin(game_config: &mut GameConfig) -> Result<(), Box Result<(), Box Result<(), Box break, context::ControlFlag::SwitchLevel(level) => { context.as_mut().current_level = level; + context.as_mut().player_progress.save(); } context::ControlFlag::UpdateLevelStart(time) => { context.as_mut().level_start_time = time; + context.as_mut().player_progress.save(); + } + context::ControlFlag::SaveProgress => { + context.as_mut().player_progress.save(); + } + context::ControlFlag::MaybeUpdateHighScore(level, time) => { + context + .as_mut() + .player_progress + .maybe_write_new_time(level, &time); } } } @@ -333,5 +350,6 @@ pub async fn game_begin(game_config: &mut GameConfig) -> Result<(), Box, +} + +impl ProgressData { + pub fn get_level_best_time(&self, level: usize) -> Option { + let level_best_time = self.level_best_times.get(&level); + match level_best_time { + Some(time) => Some(Duration::seconds(*time)), + None => None, + } + } + + pub fn maybe_write_new_time(&mut self, level: usize, time: &Duration) { + let time_in_seconds = time.num_seconds(); + if let Some(best_time) = self.get_level_best_time(level) { + if best_time.num_seconds() > time_in_seconds { + self.level_best_times.insert(level, time_in_seconds); + } + } else { + self.level_best_times.insert(level, time_in_seconds); + } + } + + pub fn load_from_file() -> Self { + info!("Loading progress data from file"); + serde_json::from_str( + &std::fs::read_to_string("./savegame.json") + .unwrap_or("{\"level_best_times\":{}}".to_string()), + ) + .unwrap_or(Self::default()) + } + + pub fn save(&self) { + info!("Saving progress data to file"); + std::fs::write("./savegame.json", serde_json::to_string(self).unwrap()).unwrap() + } +} diff --git a/game/src/scenes/ingame_scene/mod.rs b/game/src/scenes/ingame_scene/mod.rs index 798a098..8577c83 100644 --- a/game/src/scenes/ingame_scene/mod.rs +++ b/game/src/scenes/ingame_scene/mod.rs @@ -104,7 +104,12 @@ impl Action for InGameScreen { if self.current_level_idx != context.current_level { self.current_level_idx = context.current_level; self.level_switch_timestamp = Utc::now(); - context.flag_send.send(Some(ControlFlag::UpdateLevelStart(self.level_switch_timestamp))).unwrap(); + context + .flag_send + .send(Some(ControlFlag::UpdateLevelStart( + self.level_switch_timestamp, + ))) + .unwrap(); } // Grab exclusive access to the renderer @@ -128,9 +133,26 @@ impl Action for InGameScreen { // Render the HUD self.render_screen_space(&mut renderer, &context.config); + // Check if the player won let cur_level = self.levels.get(context.current_level).unwrap(); if self.player.position.x > cur_level.zones.win.x { + // Save the current time + let elapsed = Utc::now() - self.level_switch_timestamp; + context + .flag_send + .send(Some(ControlFlag::MaybeUpdateHighScore( + self.current_level_idx, + elapsed, + ))) + .unwrap(); + + // Save the progress + context + .flag_send + .send(Some(ControlFlag::SaveProgress)) + .unwrap(); + // If this is the last level, win the game if self.current_level_idx >= self.levels.len() - 1 { return Ok(ActionFlag::SwitchState(Scenes::WinScreen)); @@ -141,7 +163,6 @@ impl Action for InGameScreen { .send(Some(ControlFlag::SwitchLevel(self.current_level_idx + 1))) .unwrap(); - // TODO: This is where the timer should reset and publish state return Ok(ActionFlag::SwitchState(Scenes::NextLevelScreen)); } } diff --git a/game/src/scenes/next_level_screen.rs b/game/src/scenes/next_level_screen.rs index 5714600..b677855 100644 --- a/game/src/scenes/next_level_screen.rs +++ b/game/src/scenes/next_level_screen.rs @@ -1,6 +1,6 @@ use std::ops::{Div, Sub}; -use chrono::{DateTime, Utc}; +use chrono::{DateTime, Duration, Utc}; use dirty_fsm::{Action, ActionFlag}; use discord_sdk::activity::{ActivityBuilder, Assets}; use pkg_version::pkg_version_major; @@ -24,6 +24,9 @@ use tracing::{debug, error, info, trace}; #[derive(Debug)] pub struct NextLevelScreen { is_next_pressed: bool, + screen_load_time: DateTime, + attempt_time: String, + best_time: String, } impl NextLevelScreen { @@ -31,6 +34,9 @@ impl NextLevelScreen { pub fn new() -> Self { Self { is_next_pressed: false, + screen_load_time: Utc::now(), + attempt_time: String::new(), + best_time: String::new(), } } } @@ -43,6 +49,7 @@ impl Action for NextLevelScreen { fn on_first_run(&mut self, context: &GameContext) -> Result<(), ScreenError> { debug!("Running NextLevelScreen for the first time"); + self.screen_load_time = Utc::now(); if let Err(e) = context.discord_rpc_send.send(Some( ActivityBuilder::default().details("accepting fate").assets( @@ -63,6 +70,22 @@ impl Action for NextLevelScreen { trace!("execute() called on NextLevelScreen"); self.render_screen_space(&mut context.renderer.borrow_mut(), &context.config); + let attempt_elapsed = self.screen_load_time - context.level_start_time; + self.attempt_time = format!( + "{:02}:{:02}", + attempt_elapsed.num_minutes(), + attempt_elapsed.num_seconds() % 60 + ); + let best_time = context + .player_progress + .get_level_best_time(context.current_level) + .unwrap_or(attempt_elapsed); + self.best_time = format!( + "{:02}:{:02}", + best_time.num_minutes(), + best_time.num_seconds() % 60 + ); + if self.is_next_pressed { Ok(ActionFlag::SwitchState(Scenes::InGameScene)) } else { @@ -114,7 +137,14 @@ impl ScreenSpaceRender for NextLevelScreen { //Time raylib.draw_rgb_split_text( Vector2::new(80.0, screen_size.y / 2.0 - 40.0), - "YOUR TIME: ", + &format!("YOUR TIME: {}", self.attempt_time), + 20, + false, + Color::WHITE, + ); + raylib.draw_rgb_split_text( + Vector2::new(80.0, screen_size.y / 2.0 - 20.0), + &format!("BEST TIME: {}", self.best_time), 20, false, Color::WHITE,