commit
8a911eaacf
1
.gitignore
vendored
1
.gitignore
vendored
@ -10,3 +10,4 @@ Cargo.lock
|
||||
**/*.rs.bk
|
||||
|
||||
/*.gif
|
||||
/savegame.json
|
||||
|
@ -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"
|
||||
|
@ -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<Utc>)
|
||||
UpdateLevelStart(DateTime<Utc>),
|
||||
SaveProgress,
|
||||
MaybeUpdateHighScore(usize, Duration)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct GameContext {
|
||||
pub renderer: RefCell<HackedRaylibHandle>,
|
||||
pub config: GameConfig,
|
||||
pub player_progress: ProgressData,
|
||||
pub current_level: usize,
|
||||
pub level_start_time: DateTime<Utc>,
|
||||
pub discord_rpc_send: Sender<Option<ActivityBuilder>>,
|
||||
|
@ -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<dyn std::error::Error>> {
|
||||
@ -148,6 +150,9 @@ pub async fn game_begin(game_config: &mut GameConfig) -> Result<(), Box<dyn std:
|
||||
// Build an MPSC for signaling the control thread
|
||||
let (send_control_signal, recv_control_signal) = std::sync::mpsc::channel();
|
||||
|
||||
// Load the savefile
|
||||
let mut save_file = ProgressData::load_from_file();
|
||||
|
||||
let mut context;
|
||||
let raylib_thread;
|
||||
{
|
||||
@ -172,6 +177,7 @@ pub async fn game_begin(game_config: &mut GameConfig) -> Result<(), Box<dyn std:
|
||||
renderer: RefCell::new(rl.into()),
|
||||
config: game_config.clone(),
|
||||
current_level: 0,
|
||||
player_progress: save_file,
|
||||
level_start_time: Utc::now(),
|
||||
discord_rpc_send: send_discord_rpc,
|
||||
flag_send: send_control_signal,
|
||||
@ -320,9 +326,20 @@ pub async fn game_begin(game_config: &mut GameConfig) -> Result<(), Box<dyn std:
|
||||
context::ControlFlag::Quit => 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<dyn std:
|
||||
}
|
||||
}
|
||||
}
|
||||
context.as_mut().player_progress.save();
|
||||
Ok(())
|
||||
}
|
||||
|
44
game/src/progress.rs
Normal file
44
game/src/progress.rs
Normal file
@ -0,0 +1,44 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use chrono::Duration;
|
||||
use tracing::info;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Default)]
|
||||
pub struct ProgressData {
|
||||
pub level_best_times: HashMap<usize, i64>,
|
||||
}
|
||||
|
||||
impl ProgressData {
|
||||
pub fn get_level_best_time(&self, level: usize) -> Option<Duration> {
|
||||
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()
|
||||
}
|
||||
}
|
@ -104,7 +104,12 @@ impl Action<Scenes, ScreenError, GameContext> 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<Scenes, ScreenError, GameContext> 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<Scenes, ScreenError, GameContext> 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));
|
||||
}
|
||||
}
|
||||
|
@ -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<Utc>,
|
||||
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<Scenes, ScreenError, GameContext> 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<Scenes, ScreenError, GameContext> 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,
|
||||
|
Reference in New Issue
Block a user