Merge branch 'main_menu_screen' of https://github.com/Ewpratten/ludum-dare-49 into main_menu_screen

This commit is contained in:
Luna 2021-10-02 19:10:30 -04:00
commit d498690298
32 changed files with 669 additions and 73 deletions

9
.vscode/tasks.json vendored
View File

@ -45,6 +45,15 @@
"env": { "env": {
"RUST_LOG": "trace" "RUST_LOG": "trace"
} }
},
{
"type": "cargo",
"subcommand": "build",
"problemMatcher": [
"$rustc"
],
"group": "build",
"label": "Rust: cargo build - ludum-dare-49"
} }
] ]
} }

View File

@ -1,4 +1,4 @@
# ludum-dare-49 [WIP] # [data::loss]
[![Build](https://github.com/Ewpratten/ludum-dare-49/actions/workflows/build.yml/badge.svg)](https://github.com/Ewpratten/ludum-dare-49/actions/workflows/build.yml) [![Build](https://github.com/Ewpratten/ludum-dare-49/actions/workflows/build.yml/badge.svg)](https://github.com/Ewpratten/ludum-dare-49/actions/workflows/build.yml)
[![Clippy check](https://github.com/Ewpratten/ludum-dare-49/actions/workflows/clippy.yml/badge.svg)](https://github.com/Ewpratten/ludum-dare-49/actions/workflows/clippy.yml) [![Clippy check](https://github.com/Ewpratten/ludum-dare-49/actions/workflows/clippy.yml/badge.svg)](https://github.com/Ewpratten/ludum-dare-49/actions/workflows/clippy.yml)
[![Ludum Dare 49](https://img.shields.io/badge/Ludum%20Dare-49-orange)](https://ldjam.com/events/ludum-dare/49/$261521) [![Ludum Dare 49](https://img.shields.io/badge/Ludum%20Dare-49-orange)](https://ldjam.com/events/ludum-dare/49/$261521)

View File

@ -32,6 +32,7 @@ cfg-if = "1.0"
num-derive = "0.3" num-derive = "0.3"
num = "0.4" num = "0.4"
tiled = { version ="0.9.5", default-features = false } tiled = { version ="0.9.5", default-features = false }
async-trait = "0.1.51"
[dev-dependencies] [dev-dependencies]
puffin_viewer = "0.6" puffin_viewer = "0.6"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

View File

@ -1,5 +1,5 @@
{ {
"name": "Unnamed game", "name": "data::loss",
"base_window_size": [ "base_window_size": [
1080, 1080,
720 720

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View File

@ -0,0 +1,32 @@
[
{
"x": 322,
"y": 926,
"width": 610,
"height": 73
},
{
"x": 852,
"y": 882,
"width": 81,
"height": 178
},
{
"x": 852,
"y": 882,
"width": 258,
"height": 33
},
{
"x": 1599,
"y": 699,
"width": 918,
"height": 63
},
{
"x": 2733,
"y": 789,
"width": 108,
"height": 211
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1,3 @@
[
"level_0"
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

View File

@ -1,12 +1,26 @@
use std::ops::Mul;
use raylib::math::{Rectangle, Vector2}; use raylib::math::{Rectangle, Vector2};
use crate::scenes::ingame_scene::world::WORLD_LEVEL_X_OFFSET;
use super::{CharacterState, MainCharacter}; use super::{CharacterState, MainCharacter};
const GRAVITY_PPS: f32 = 2.0; pub const GRAVITY_PPS: f32 = 2.0;
pub fn modify_player_based_on_forces(player: &mut MainCharacter) -> Result<(), ()> { pub fn modify_player_based_on_forces(
// Convert the player to a rectangle player: &mut MainCharacter,
colliders: &Vec<Rectangle>,
level_height_offset: f32,
) -> Result<(), ()> {
// Modify the player's velocity by the forces
player.movement_force += player.base_velocity;
player.velocity = player.movement_force;
// Predict the player's position next frame
let predicted_player_position = player.position + player.velocity; let predicted_player_position = player.position + player.velocity;
// Calculate a bounding rect around the player both now, and one frame in the future
let player_rect = Rectangle::new( let player_rect = Rectangle::new(
predicted_player_position.x - (player.size.x / 2.0), predicted_player_position.x - (player.size.x / 2.0),
predicted_player_position.y - (player.size.x / 2.0), predicted_player_position.y - (player.size.x / 2.0),
@ -17,24 +31,44 @@ pub fn modify_player_based_on_forces(player: &mut MainCharacter) -> Result<(), (
// Calculate a generic "floor" to always collide with // Calculate a generic "floor" to always collide with
let floor_rect = Rectangle::new(f32::MIN, 0.0, f32::MAX, 1.0); let floor_rect = Rectangle::new(f32::MIN, 0.0, f32::MAX, 1.0);
// Check collision conditions
let check_player_colliding_with_floor = || floor_rect.check_collision_recs(&player_rect);
let check_player_colliding_with_floor_next_frame =
|| player_rect.y + player_rect.height > floor_rect.y;
let check_player_colliding_with_colliders = || {
colliders.iter().any(|rect| {
let mut translated_rect = rect.clone();
translated_rect.y += level_height_offset;
translated_rect.x += WORLD_LEVEL_X_OFFSET;
translated_rect.check_collision_recs(&player_rect)
})
};
// If the player is colliding, only apply the x force // If the player is colliding, only apply the x force
if (floor_rect.check_collision_recs(&player_rect) || player_rect.y + player_rect.height > floor_rect.y) if (check_player_colliding_with_floor()
&& player.velocity.y > 0.0 || check_player_colliding_with_floor_next_frame()
|| check_player_colliding_with_colliders())
&& player.velocity.y != 0.0
{ {
player.velocity.y = 0.0; player.velocity.y = 0.0;
// Handle ending a jump // Handle ending a jump
if player.current_state == CharacterState::Jumping { if player.current_state == CharacterState::Jumping
player.set_state(CharacterState::Running); || player.current_state == CharacterState::Dashing
{
player.update_player(
Some(CharacterState::Running),
colliders,
level_height_offset,
);
return Ok(());
} }
} }
// TODO: Error out if colliding in the X direction // Check sideways collisions
// Apply the force // Finally apply the velocity to the player
player.position += player.velocity; player.position += player.velocity;
// Apply gravity
player.velocity.y += GRAVITY_PPS;
Ok(()) Ok(())
} }

View File

@ -2,13 +2,16 @@ pub mod collisions;
pub mod render; pub mod render;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use raylib::{math::Vector2, texture::Texture2D}; use raylib::{
math::{Rectangle, Vector2},
texture::Texture2D,
};
use crate::utilities::anim_render::AnimatedSpriteSheet; use crate::utilities::anim_render::AnimatedSpriteSheet;
use self::collisions::modify_player_based_on_forces; use self::collisions::{modify_player_based_on_forces, GRAVITY_PPS};
#[derive(Debug, Default, PartialEq, Eq)] #[derive(Debug, Default, PartialEq, Eq, Clone)]
pub enum CharacterState { pub enum CharacterState {
#[default] #[default]
Running, Running,
@ -19,6 +22,8 @@ pub enum CharacterState {
#[derive(Debug)] #[derive(Debug)]
pub struct MainCharacter { pub struct MainCharacter {
pub position: Vector2, pub position: Vector2,
pub movement_force: Vector2,
pub base_velocity: Vector2,
pub velocity: Vector2, pub velocity: Vector2,
pub size: Vector2, pub size: Vector2,
pub sprite_sheet: AnimatedSpriteSheet, pub sprite_sheet: AnimatedSpriteSheet,
@ -30,8 +35,10 @@ impl MainCharacter {
pub fn new(position: Vector2, sprite_sheet: Texture2D) -> Self { pub fn new(position: Vector2, sprite_sheet: Texture2D) -> Self {
Self { Self {
position, position,
velocity: Vector2::new(20.0, 0.0), movement_force: Vector2::zero(),
size: Vector2::new(100.0, 130.0), velocity: Vector2::zero(),
base_velocity: Vector2::new(0.0, GRAVITY_PPS),
size: Vector2::new(100.0, 100.0),
sprite_sheet: AnimatedSpriteSheet::new( sprite_sheet: AnimatedSpriteSheet::new(
sprite_sheet, sprite_sheet,
Vector2::new(300.0, 300.0), Vector2::new(300.0, 300.0),
@ -44,20 +51,28 @@ impl MainCharacter {
} }
} }
pub fn apply_force(&mut self, force: Vector2) -> Option<()> { pub fn update_player(
self.velocity = force; &mut self,
modify_player_based_on_forces(self).unwrap(); state: Option<CharacterState>,
Some(()) colliders: &Vec<Rectangle>,
} level_height_offset: f32,
) {
if let Some(state) = state {
// Update the internal state
if state != self.current_state {
self.current_state = state.clone();
self.state_set_timestamp = Utc::now();
}
pub fn update_gravity(&mut self) { // Handle extra external forces based on the character state
modify_player_based_on_forces(self).unwrap(); self.movement_force = match state {
} CharacterState::Running => Vector2::new(12.0, 0.0),
CharacterState::Jumping => Vector2::new(12.0, -30.0),
pub fn set_state(&mut self, state: CharacterState) { CharacterState::Dashing => Vector2::new(30.0, -20.0),
if state != self.current_state { };
self.current_state = state;
self.state_set_timestamp = Utc::now();
} }
// Update the player based on the new velocity
modify_player_based_on_forces(self, colliders, level_height_offset).unwrap();
} }
} }

View File

@ -1,4 +1,4 @@
use std::ops::{Div, Sub}; use std::ops::{Add, Div, Mul, Sub};
use chrono::Utc; use chrono::Utc;
use raylib::prelude::*; use raylib::prelude::*;
@ -37,4 +37,16 @@ pub fn render_character_in_camera_space(
Some(Vector2::new(player.size.y, player.size.y)), Some(Vector2::new(player.size.y, player.size.y)),
Some(frame_id), Some(frame_id),
); );
// Possibly render a debug vector
if config.debug_view {
raylib.draw_line_v(
player.position.sub(player.size.div(2.0)),
player
.position
.sub(player.size.div(2.0))
.add(player.velocity.mul(10.0).add(Vector2::new(0.0, 100.0))),
Color::RED,
);
}
} }

View File

@ -1,4 +1,6 @@
use std::cell::RefCell; use std::{cell::RefCell, sync::mpsc::Sender};
use discord_sdk::activity::ActivityBuilder;
use crate::{GameConfig, utilities::non_ref_raylib::HackedRaylibHandle}; use crate::{GameConfig, utilities::non_ref_raylib::HackedRaylibHandle};
@ -6,7 +8,8 @@ use crate::{GameConfig, utilities::non_ref_raylib::HackedRaylibHandle};
#[derive(Debug)] #[derive(Debug)]
pub struct GameContext { pub struct GameContext {
pub renderer: RefCell<HackedRaylibHandle>, pub renderer: RefCell<HackedRaylibHandle>,
pub config: GameConfig pub config: GameConfig,
pub discord_rpc_send: Sender<Option<ActivityBuilder>>
} }
// impl GameContext { // impl GameContext {

View File

@ -1,6 +1,7 @@
#![feature(derive_default_enum)] #![feature(derive_default_enum)]
#![feature(custom_inner_attributes)] #![feature(custom_inner_attributes)]
#![feature(stmt_expr_attributes)] #![feature(stmt_expr_attributes)]
#![feature(async_await)]
#![feature(c_variadic)] #![feature(c_variadic)]
#![deny(unsafe_code)] #![deny(unsafe_code)]
#![warn( #![warn(
@ -69,7 +70,7 @@
)] )]
#![clippy::msrv = "1.57.0"] #![clippy::msrv = "1.57.0"]
use std::cell::RefCell; use std::{cell::RefCell, sync::mpsc::TryRecvError};
use discord_sdk::activity::ActivityBuilder; use discord_sdk::activity::ActivityBuilder;
use raylib::prelude::*; use raylib::prelude::*;
@ -97,6 +98,8 @@ extern crate serde;
extern crate approx; extern crate approx;
#[macro_use] #[macro_use]
extern crate num_derive; extern crate num_derive;
#[macro_use]
extern crate async_trait;
mod context; mod context;
mod discord_rpc; mod discord_rpc;
@ -106,7 +109,7 @@ pub use utilities::{datastore::StaticGameData, game_config::GameConfig};
mod character; mod character;
/// The game entrypoint /// The game entrypoint
pub async fn game_begin(game_config: &GameConfig) -> Result<(), Box<dyn std::error::Error>> { pub async fn game_begin(game_config: &mut GameConfig) -> Result<(), Box<dyn std::error::Error>> {
// Set up profiling // Set up profiling
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
let _puffin_server = let _puffin_server =
@ -133,11 +136,14 @@ pub async fn game_begin(game_config: &GameConfig) -> Result<(), Box<dyn std::err
}; };
maybe_set_discord_presence( maybe_set_discord_presence(
&discord_rpc, &discord_rpc,
ActivityBuilder::default().details("Testing..."), ActivityBuilder::default().details("Game starting"),
) )
.await .await
.unwrap(); .unwrap();
// Build an MPSC for the game to send rich presence data to discord
let (send_discord_rpc, recv_discord_rpc) = std::sync::mpsc::channel();
let context; let context;
let raylib_thread; let raylib_thread;
{ {
@ -148,7 +154,7 @@ pub async fn game_begin(game_config: &GameConfig) -> Result<(), Box<dyn std::err
game_config.base_window_size.0, game_config.base_window_size.0,
game_config.base_window_size.1, game_config.base_window_size.1,
) )
.title(&game_config.name) .title(&format!("[{}]", game_config.name))
.vsync() .vsync()
.msaa_4x() .msaa_4x()
.resizable() .resizable()
@ -161,6 +167,7 @@ pub async fn game_begin(game_config: &GameConfig) -> Result<(), Box<dyn std::err
context = Box::new(GameContext { context = Box::new(GameContext {
renderer: RefCell::new(rl.into()), renderer: RefCell::new(rl.into()),
config: game_config.clone(), config: game_config.clone(),
discord_rpc_send: send_discord_rpc,
}); });
} }
@ -198,7 +205,6 @@ pub async fn game_begin(game_config: &GameConfig) -> Result<(), Box<dyn std::err
&raylib_thread, &raylib_thread,
)?; )?;
while !context.renderer.borrow().window_should_close() { while !context.renderer.borrow().window_should_close() {
// Profile the main game loop // Profile the main game loop
puffin::profile_scope!("main_loop"); puffin::profile_scope!("main_loop");
@ -209,6 +215,27 @@ pub async fn game_begin(game_config: &GameConfig) -> Result<(), Box<dyn std::err
.update(&mut context.renderer.borrow_mut(), &raylib_thread) .update(&mut context.renderer.borrow_mut(), &raylib_thread)
.unwrap(); .unwrap();
// If in dev mode, allow a debug key
#[cfg(debug_assertions)]
{
if context
.renderer
.borrow()
.is_key_pressed(KeyboardKey::KEY_F3)
{
game_config.debug_view = !game_config.debug_view;
}
}
// Handle fullscreen shortcut
if context
.renderer
.borrow()
.is_key_pressed(KeyboardKey::KEY_F11)
{
context.renderer.borrow_mut().toggle_fullscreen();
}
// Switch into draw mode the unsafe way (using unsafe code here to avoid borrow checker hell) // Switch into draw mode the unsafe way (using unsafe code here to avoid borrow checker hell)
#[allow(unsafe_code)] #[allow(unsafe_code)]
unsafe { unsafe {
@ -260,6 +287,22 @@ pub async fn game_begin(game_config: &GameConfig) -> Result<(), Box<dyn std::err
unsafe { unsafe {
raylib::ffi::EndDrawing(); raylib::ffi::EndDrawing();
} }
// Try to update discord
match recv_discord_rpc.try_recv() {
Ok(activity) => {
if let Some(activity) = activity {
if let Err(e) = maybe_set_discord_presence(&discord_rpc, activity).await {
error!("Failed to update discord presence: {:?}", e);
}
}
}
Err(TryRecvError::Empty) => {}
Err(TryRecvError::Disconnected) => {
error!("Discord RPC channel disconnected");
continue;
}
}
} }
Ok(()) Ok(())
} }

View File

@ -1,6 +1,7 @@
use dirty_fsm::{Action, ActionFlag}; use dirty_fsm::{Action, ActionFlag};
use discord_sdk::activity::{ActivityBuilder, Assets};
use raylib::{color::Color, prelude::RaylibDraw}; use raylib::{color::Color, prelude::RaylibDraw};
use tracing::{debug, trace}; use tracing::{debug, error, trace};
use crate::{ use crate::{
context::GameContext, context::GameContext,
@ -26,8 +27,20 @@ impl Action<Scenes, ScreenError, GameContext> for FsmErrorScreen {
Ok(()) Ok(())
} }
fn on_first_run(&mut self, _context: &GameContext) -> Result<(), ScreenError> { fn on_first_run(&mut self, context: &GameContext) -> Result<(), ScreenError> {
debug!("Running FsmErrorScreen for the first time"); debug!("Running FsmErrorScreen for the first time");
// Update discord
if let Err(e) = context.discord_rpc_send.send(Some(
ActivityBuilder::default()
.details("IT FUCKING DIED")
.assets(
Assets::default().large("game-logo-small", Some(context.config.name.clone())),
),
)) {
error!("Failed to update discord: {}", e);
}
Ok(()) Ok(())
} }

View File

@ -0,0 +1,53 @@
use raylib::{RaylibHandle, RaylibThread};
use crate::{
utilities::datastore::{load_texture_from_internal_data, ResourceLoadError},
StaticGameData,
};
use super::Level;
pub fn load_all_levels(
raylib_handle: &mut RaylibHandle,
thread: &RaylibThread,
) -> Result<Vec<Level>, ResourceLoadError> {
// Get a listing of all levels we have
let level_names: Vec<String> = serde_json::from_str(
&String::from_utf8(
StaticGameData::get("levels/levels.json")
.expect("Could not load levels.json")
.data
.into(),
)
.unwrap(),
)?;
// Build a level list
let mut levels = Vec::new();
for level_name in &level_names {
levels.push(Level {
name: level_name.to_string(),
background_tex: load_texture_from_internal_data(
raylib_handle,
thread,
&format!("levels/{}/background.png", level_name),
)?,
platform_tex: load_texture_from_internal_data(
raylib_handle,
thread,
&format!("levels/{}/platforms.png", level_name),
)?,
colliders: serde_json::from_str(
&String::from_utf8(
StaticGameData::get(&format!("levels/{}/colliders.json", level_name))
.unwrap()
.data
.into(),
)
.unwrap(),
)?,
});
}
Ok(levels)
}

View File

@ -0,0 +1,11 @@
use raylib::{math::Rectangle, texture::Texture2D};
pub mod loader;
#[derive(Debug)]
pub struct Level {
pub name: String,
pub background_tex: Texture2D,
pub platform_tex: Texture2D,
pub colliders: Vec<Rectangle>
}

View File

@ -1,24 +1,42 @@
use dirty_fsm::{Action, ActionFlag}; use dirty_fsm::{Action, ActionFlag};
use discord_sdk::activity::{ActivityBuilder, Assets};
use raylib::prelude::*; use raylib::prelude::*;
use crate::{character::{CharacterState, MainCharacter}, context::GameContext, utilities::render_layer::{FrameUpdate, ScreenSpaceRender, WorldSpaceRender}}; use crate::{
character::{CharacterState, MainCharacter},
context::GameContext,
utilities::{
render_layer::{FrameUpdate, ScreenSpaceRender, WorldSpaceRender},
world_paint_texture::WorldPaintTexture,
},
};
use self::level::Level;
use super::{Scenes, ScreenError}; use super::{Scenes, ScreenError};
use tracing::{debug, trace}; use tracing::{debug, error, trace};
mod hud; mod hud;
pub mod level;
mod update; mod update;
mod world; pub mod world;
#[derive(Debug)] #[derive(Debug)]
pub struct InGameScreen { pub struct InGameScreen {
camera: Camera2D, camera: Camera2D,
player: MainCharacter, player: MainCharacter,
world_background: WorldPaintTexture,
levels: Vec<Level>,
current_level_idx: usize,
} }
impl InGameScreen { impl InGameScreen {
/// Construct a new `InGameScreen` /// Construct a new `InGameScreen`
pub fn new(player_sprite_sheet: Texture2D) -> Self { pub fn new(
player_sprite_sheet: Texture2D,
background_texture: Texture2D,
levels: Vec<Level>,
) -> Self {
Self { Self {
camera: Camera2D { camera: Camera2D {
offset: Vector2::zero(), offset: Vector2::zero(),
@ -26,7 +44,10 @@ impl InGameScreen {
rotation: 0.0, rotation: 0.0,
zoom: 1.0, zoom: 1.0,
}, },
player: MainCharacter::new(Vector2::new(0.0, -80.0), player_sprite_sheet), player: MainCharacter::new(Vector2::new(0.0, -85.0), player_sprite_sheet),
world_background: WorldPaintTexture::new(background_texture),
levels,
current_level_idx: 0,
} }
} }
} }
@ -37,11 +58,25 @@ impl Action<Scenes, ScreenError, GameContext> for InGameScreen {
Ok(()) Ok(())
} }
fn on_first_run(&mut self, _context: &GameContext) -> Result<(), ScreenError> { fn on_first_run(&mut self, context: &GameContext) -> Result<(), ScreenError> {
debug!("Running InGameScreen for the first time"); debug!("Running InGameScreen for the first time");
// Set the player to running // Set the player to running
self.player.set_state(CharacterState::Running); let cur_level = self.levels.get(self.current_level_idx).unwrap();
self.player.update_player(
Some(CharacterState::Running),
&cur_level.colliders,
-cur_level.platform_tex.height as f32,
);
// Update discord
if let Err(e) = context.discord_rpc_send.send(Some(
ActivityBuilder::default().details("in game").assets(
Assets::default().large("game-logo-small", Some(context.config.name.clone())),
),
)) {
error!("Failed to update discord: {}", e);
}
Ok(()) Ok(())
} }
@ -75,7 +110,11 @@ impl Action<Scenes, ScreenError, GameContext> for InGameScreen {
// Render the HUD // Render the HUD
self.render_screen_space(&mut renderer, &context.config); self.render_screen_space(&mut renderer, &context.config);
Ok(ActionFlag::Continue) if renderer.is_key_pressed(KeyboardKey::KEY_ESCAPE) {
Ok(ActionFlag::SwitchState(Scenes::PauseScreen))
} else {
Ok(ActionFlag::Continue)
}
} }
fn on_finish(&mut self, _interrupted: bool) -> Result<(), ScreenError> { fn on_finish(&mut self, _interrupted: bool) -> Result<(), ScreenError> {

View File

@ -17,26 +17,37 @@ impl FrameUpdate for InGameScreen {
config: &GameConfig, config: &GameConfig,
) { ) {
puffin::profile_function!(); puffin::profile_function!();
// Get the current level
let cur_level = self.levels.get(self.current_level_idx).unwrap();
// Set the camera's offset based on screen size // Set the camera's offset based on screen size
self.camera.offset = raylib.get_screen_size().div(Vector2::new(2.0, 1.05)); self.camera.offset = raylib.get_screen_size().div(Vector2::new(2.0, 1.05));
self.camera.target = Vector2::new(self.player.position.x, self.camera.target.y); self.camera.target = Vector2::new(self.player.position.x, self.camera.target.y);
// Check the only possible keyboard inputs // Check the only possible keyboard inputs
let is_jump = raylib.is_key_pressed(KeyboardKey::KEY_SPACE); let is_jump = raylib.is_key_pressed(KeyboardKey::KEY_SPACE)
let is_dash = raylib.is_key_pressed(KeyboardKey::KEY_LEFT_SHIFT); && !(self.player.current_state == CharacterState::Jumping);
let is_pause = raylib.is_key_pressed(KeyboardKey::KEY_ESCAPE); let is_dash = raylib.is_key_pressed(KeyboardKey::KEY_LEFT_SHIFT)
&& !(self.player.current_state == CharacterState::Dashing);
if is_jump { if is_jump {
self.player.apply_force(Vector2::new(0.0, -30.0)); self.player.update_player(Some(CharacterState::Jumping), &cur_level.colliders,
self.player.set_state(CharacterState::Jumping); -cur_level.platform_tex.height as f32,);
} else if is_dash { } else if is_dash {
self.player.apply_force(Vector2::new(40.0, -10.0)); self.player.update_player(Some(CharacterState::Dashing), &cur_level.colliders,
self.player.set_state(CharacterState::Dashing); -cur_level.platform_tex.height as f32,);
} else { } else {
if self.player.current_state != CharacterState::Jumping { if self.player.current_state != CharacterState::Jumping
self.player.set_state(CharacterState::Running); && self.player.current_state != CharacterState::Dashing
{
self.player.update_player(Some(CharacterState::Running), &cur_level.colliders,
-cur_level.platform_tex.height as f32,);
} else {
self.player.update_player(None, &cur_level.colliders,
-cur_level.platform_tex.height as f32,);
} }
} }
self.player.update_gravity();
} }
} }

View File

@ -8,6 +8,8 @@ use crate::{
}; };
use raylib::prelude::*; use raylib::prelude::*;
pub const WORLD_LEVEL_X_OFFSET: f32 = 200.0;
impl WorldSpaceRender for InGameScreen { impl WorldSpaceRender for InGameScreen {
fn render_world_space( fn render_world_space(
&mut self, &mut self,
@ -15,8 +17,15 @@ impl WorldSpaceRender for InGameScreen {
config: &GameConfig, config: &GameConfig,
) { ) {
puffin::profile_function!(); puffin::profile_function!();
// Render the player
render_character_in_camera_space(raylib, &self.player, &config); // Get the current level
let cur_level = self.levels.get(self.current_level_idx).unwrap();
// Render the world background
// self.world_background.render(raylib, Vector2::new(0.0, -1080.0), &self.camera);
// Render the platform layer
raylib.draw_texture_v(&cur_level.platform_tex, Vector2::new(WORLD_LEVEL_X_OFFSET, -cur_level.platform_tex.height as f32), Color::WHITE);
// Render the floor as a line // Render the floor as a line
let screen_world_zero = raylib.get_screen_to_world2D(Vector2::zero(), self.camera); let screen_world_zero = raylib.get_screen_to_world2D(Vector2::zero(), self.camera);
@ -30,5 +39,9 @@ impl WorldSpaceRender for InGameScreen {
5, 5,
config.colors.white, config.colors.white,
); );
// Render the player
render_character_in_camera_space(raylib, &self.player, &config);
} }
} }

View File

@ -3,6 +3,7 @@ use std::ops::{Div, Sub};
use cfg_if::cfg_if; use cfg_if::cfg_if;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use dirty_fsm::{Action, ActionFlag}; use dirty_fsm::{Action, ActionFlag};
use discord_sdk::activity::{ActivityBuilder, Assets};
use raylib::prelude::*; use raylib::prelude::*;
use crate::{GameConfig, context::GameContext, utilities::{ use crate::{GameConfig, context::GameContext, utilities::{
@ -13,7 +14,7 @@ use crate::{GameConfig, context::GameContext, utilities::{
}}; }};
use super::{Scenes, ScreenError}; use super::{Scenes, ScreenError};
use tracing::{debug, info, trace}; use tracing::{debug, info, error, trace};
/// Defines how long the loading screen should be displayed. /// Defines how long the loading screen should be displayed.
const LOADING_SCREEN_DURATION_SECONDS: u8 = 3; const LOADING_SCREEN_DURATION_SECONDS: u8 = 3;
@ -49,9 +50,18 @@ impl Action<Scenes, ScreenError, GameContext> for LoadingScreen {
Ok(()) Ok(())
} }
fn on_first_run(&mut self, _context: &GameContext) -> Result<(), ScreenError> { fn on_first_run(&mut self, context: &GameContext) -> Result<(), ScreenError> {
debug!("Running LoadingScreen for the first time"); debug!("Running LoadingScreen for the first time");
// Update discord
if let Err(e) = context.discord_rpc_send.send(Some(
ActivityBuilder::default().details("loading...").assets(
Assets::default().large("game-logo-small", Some(context.config.name.clone())),
),
)) {
error!("Failed to update discord: {}", e);
}
// Keep track of when this screen is opened // Keep track of when this screen is opened
self.start_timestamp = Some(Utc::now()); self.start_timestamp = Some(Utc::now());

View File

@ -2,19 +2,24 @@ use std::ops::{Div, Sub};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use dirty_fsm::{Action, ActionFlag}; use dirty_fsm::{Action, ActionFlag};
use discord_sdk::activity::{ActivityBuilder, Assets};
use pkg_version::pkg_version_major; use pkg_version::pkg_version_major;
use raylib::prelude::*; use raylib::prelude::*;
use crate::{GameConfig, context::GameContext, utilities::{ use crate::{
context::GameContext,
utilities::{
datastore::{load_texture_from_internal_data, ResourceLoadError}, datastore::{load_texture_from_internal_data, ResourceLoadError},
game_version::get_version_string, game_version::get_version_string,
math::interpolate_exp, math::interpolate_exp,
non_ref_raylib::HackedRaylibHandle, non_ref_raylib::HackedRaylibHandle,
render_layer::ScreenSpaceRender, render_layer::ScreenSpaceRender,
}}; },
GameConfig,
};
use super::{Scenes, ScreenError}; use super::{Scenes, ScreenError};
use tracing::{debug, info, trace}; use tracing::{debug, error, info, trace};
#[derive(Debug)] #[derive(Debug)]
pub struct MainMenuScreen { pub struct MainMenuScreen {
@ -44,9 +49,18 @@ impl Action<Scenes, ScreenError, GameContext> for MainMenuScreen {
Ok(()) Ok(())
} }
fn on_first_run(&mut self, _context: &GameContext) -> Result<(), ScreenError> { fn on_first_run(&mut self, context: &GameContext) -> Result<(), ScreenError> {
debug!("Running MainMenuScreen for the first time"); debug!("Running MainMenuScreen for the first time");
// Update discord
if let Err(e) = context.discord_rpc_send.send(Some(
ActivityBuilder::default().details("main menu").assets(
Assets::default().large("game-logo-small", Some(context.config.name.clone())),
),
)) {
error!("Failed to update discord: {}", e);
}
Ok(()) Ok(())
} }
@ -89,7 +103,7 @@ impl ScreenSpaceRender for MainMenuScreen {
fn render_screen_space( fn render_screen_space(
&mut self, &mut self,
raylib: &mut crate::utilities::non_ref_raylib::HackedRaylibHandle, raylib: &mut crate::utilities::non_ref_raylib::HackedRaylibHandle,
config: &GameConfig config: &GameConfig,
) { ) {
// Render the background // Render the background
raylib.clear_background(Color::BLACK); raylib.clear_background(Color::BLACK);

View File

@ -1,4 +1,10 @@
use self::{fsm_error_screen::FsmErrorScreen, options_screen::OptionsScreen, how_to_play_screen::HowToPlayScreen, ingame_scene::InGameScreen, loading_screen::LoadingScreen, main_menu_screen::MainMenuScreen}; use self::{
pause_screen::PauseScreen,
fsm_error_screen::FsmErrorScreen,
ingame_scene::{level::loader::load_all_levels, InGameScreen},
loading_screen::LoadingScreen,
main_menu_screen::MainMenuScreen, options_screen::OptionsScreen, how_to_play_screen::HowToPlayScreen,
};
use crate::{ use crate::{
context::GameContext, context::GameContext,
utilities::{ utilities::{
@ -15,6 +21,7 @@ pub mod loading_screen;
pub mod main_menu_screen; pub mod main_menu_screen;
pub mod how_to_play_screen; pub mod how_to_play_screen;
pub mod options_screen; pub mod options_screen;
pub mod pause_screen;
/// Defines all scenes /// Defines all scenes
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
@ -26,6 +33,7 @@ pub enum Scenes {
InGameScene, InGameScene,
HowToPlayScreen, HowToPlayScreen,
OptionsScreen, OptionsScreen,
PauseScreen,
} }
/// Contains any possible errors thrown while rendering /// Contains any possible errors thrown while rendering
@ -47,6 +55,9 @@ pub fn build_screen_state_machine(
// Load the various textures needed by the states // Load the various textures needed by the states
let player_sprite_sheet = let player_sprite_sheet =
load_texture_from_internal_data(raylib_handle, thread, "character/player_run.png").unwrap(); load_texture_from_internal_data(raylib_handle, thread, "character/player_run.png").unwrap();
let world_background =
load_texture_from_internal_data(raylib_handle, thread, "default-texture.png").unwrap();
let levels = load_all_levels(raylib_handle, thread).unwrap();
// Set up the state machine // Set up the state machine
let mut machine = StateMachine::new(); let mut machine = StateMachine::new();
@ -56,9 +67,13 @@ pub fn build_screen_state_machine(
LoadingScreen::new(raylib_handle, thread)?, LoadingScreen::new(raylib_handle, thread)?,
)?; )?;
machine.add_action(Scenes::MainMenuScreen, MainMenuScreen::new())?; machine.add_action(Scenes::MainMenuScreen, MainMenuScreen::new())?;
machine.add_action(Scenes::InGameScene, InGameScreen::new(player_sprite_sheet))?;
machine.add_action(Scenes::HowToPlayScreen, HowToPlayScreen::new())?; machine.add_action(Scenes::HowToPlayScreen, HowToPlayScreen::new())?;
machine.add_action(Scenes::OptionsScreen, OptionsScreen::new())?; machine.add_action(Scenes::OptionsScreen, OptionsScreen::new())?;
machine.add_action(Scenes::PauseScreen, PauseScreen::new())?;
machine.add_action(
Scenes::InGameScene,
InGameScreen::new(player_sprite_sheet, world_background, levels),
)?;
Ok(machine) Ok(machine)
} }

View File

@ -0,0 +1,202 @@
use std::ops::{Div, Sub};
use chrono::{DateTime, Utc};
use dirty_fsm::{Action, ActionFlag};
use discord_sdk::activity::{ActivityBuilder, Assets};
use pkg_version::pkg_version_major;
use raylib::prelude::*;
use crate::{
context::GameContext,
utilities::{
datastore::{load_texture_from_internal_data, ResourceLoadError},
game_version::get_version_string,
math::interpolate_exp,
non_ref_raylib::HackedRaylibHandle,
render_layer::ScreenSpaceRender,
},
GameConfig,
};
use super::{Scenes, ScreenError};
use tracing::{debug, error, info, trace};
#[derive(Debug)]
pub struct PauseScreen {}
impl PauseScreen {
/// Construct a new `PauseScreen`
pub fn new() -> Self {
Self {}
}
}
impl Action<Scenes, ScreenError, GameContext> for PauseScreen {
fn on_register(&mut self) -> Result<(), ScreenError> {
debug!("Registered");
Ok(())
}
fn on_first_run(&mut self, context: &GameContext) -> Result<(), ScreenError> {
debug!("Running PauseScreen for the first time");
// Update discord
if let Err(e) = context.discord_rpc_send.send(Some(
ActivityBuilder::default().details("paused").assets(
Assets::default().large("game-logo-small", Some(context.config.name.clone())),
),
)) {
error!("Failed to update discord: {}", e);
}
Ok(())
}
fn execute(
&mut self,
_delta: &chrono::Duration,
context: &GameContext,
) -> Result<dirty_fsm::ActionFlag<Scenes>, ScreenError> {
trace!("execute() called on PauseScreen");
self.render_screen_space(&mut context.renderer.borrow_mut(), &context.config);
//Mouse Position
let mouse_position: Vector2 = context.renderer.borrow_mut().get_mouse_position();
//Mouse Input
let is_left_click = context
.renderer
.borrow_mut()
.is_mouse_button_down(MouseButton::MOUSE_LEFT_BUTTON);
//"Hitboxes" for the resume and Main menu buttons
if is_left_click
&& Rectangle::new(322.0, 321.0, 435.0, 80.0).check_collision_point_rec(mouse_position)
{
return Ok(ActionFlag::SwitchState(Scenes::InGameScene));
}
if is_left_click
&& Rectangle::new(390.0, 464.0, 200.0, 50.0).check_collision_point_rec(mouse_position)
{
return Ok(ActionFlag::SwitchState(Scenes::MainMenuScreen));
}
if context
.renderer
.borrow_mut()
.is_key_pressed(KeyboardKey::KEY_ESCAPE)
{
Ok(ActionFlag::SwitchState(Scenes::InGameScene))
} else {
Ok(ActionFlag::Continue)
}
}
fn on_finish(&mut self, _interrupted: bool) -> Result<(), ScreenError> {
debug!("Finished PauseScreen");
Ok(())
}
}
impl ScreenSpaceRender for PauseScreen {
fn render_screen_space(
&mut self,
raylib: &mut crate::utilities::non_ref_raylib::HackedRaylibHandle,
config: &GameConfig,
) {
let screen_size = raylib.get_screen_size();
// Render the background
raylib.clear_background(Color::BLACK.fade(50.0));
//Mouse Position
let mouse_position: Vector2 = raylib.get_mouse_position();
//Mouse Input
let is_left_click = raylib.is_mouse_button_down(MouseButton::MOUSE_LEFT_BUTTON);
//Pause Menu Texts With Glitchy Effect
raylib.draw_text(
"Paused",
(screen_size.x as i32 / 2) - 223,
(screen_size.y as i32 / 2) - 40,
120,
Color::RED,
);
raylib.draw_text(
"Paused",
(screen_size.x as i32 / 2) - 217,
(screen_size.y as i32 / 2) - 40,
120,
Color::BLUE,
);
raylib.draw_text(
"Paused",
(screen_size.x as i32 / 2) - 220,
(screen_size.y as i32 / 2) - 40,
120,
Color::WHITE,
);
raylib.draw_text(
"Click To Resume",
(screen_size.x as i32 / 2) - 80,
(screen_size.y as i32 / 2) + 60,
20,
Color::RED,
);
raylib.draw_text(
"Click To Resume",
(screen_size.x as i32 / 2) - 80,
(screen_size.y as i32 / 2) + 60,
20,
Color::BLUE,
);
raylib.draw_text(
"Click To Resume",
(screen_size.x as i32 / 2) - 80,
(screen_size.y as i32 / 2) + 60,
20,
Color::WHITE,
);
raylib.draw_text(
"Main Menu",
(screen_size.x as i32 / 2) - 123,
(screen_size.y as i32 / 2) + 100,
50,
Color::RED,
);
raylib.draw_text(
"Main Menu",
(screen_size.x as i32 / 2) - 117,
(screen_size.y as i32 / 2) + 100,
50,
Color::BLUE,
);
raylib.draw_text(
"Main Menu",
(screen_size.x as i32 / 2) - 120,
(screen_size.y as i32 / 2) + 100,
50,
Color::WHITE,
);
if Rectangle::new(390.0, 464.0, 200.0, 50.0).check_collision_point_rec(mouse_position) {
raylib.draw_text(
"Main Menu",
(screen_size.x as i32 / 2) - 120,
(screen_size.y as i32 / 2) + 100,
50,
Color::YELLOW,
);
}
if Rectangle::new(322.0, 321.0, 435.0, 80.0).check_collision_point_rec(mouse_position) {
raylib.draw_text(
"Paused",
(screen_size.x as i32 / 2) - 220,
(screen_size.y as i32 / 2) - 40,
120,
Color::DARKBLUE,
);
}
}
}

View File

@ -14,6 +14,8 @@ pub struct StaticGameData;
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum ResourceLoadError { pub enum ResourceLoadError {
#[error(transparent)]
JsonDeser(#[from] serde_json::Error),
#[error(transparent)] #[error(transparent)]
Io(#[from] std::io::Error), Io(#[from] std::io::Error),
#[error("Could not load embedded asset: {0}")] #[error("Could not load embedded asset: {0}")]

View File

@ -30,6 +30,9 @@ pub struct GameConfig {
pub sentry_dsn: String, pub sentry_dsn: String,
pub colors: ColorTheme, pub colors: ColorTheme,
pub animation_fps: usize, pub animation_fps: usize,
#[serde(skip)]
pub debug_view: bool
} }
impl GameConfig { impl GameConfig {

View File

@ -1,3 +1,4 @@
pub mod anim_render;
pub mod datastore; pub mod datastore;
pub mod discord; pub mod discord;
pub mod game_config; pub mod game_config;
@ -6,4 +7,4 @@ pub mod math;
pub mod non_ref_raylib; pub mod non_ref_raylib;
pub mod render_layer; pub mod render_layer;
pub mod shaders; pub mod shaders;
pub mod anim_render; pub mod world_paint_texture;

View File

@ -0,0 +1,57 @@
//! Defines a texture that tiles across the whole screen in world space
use raylib::{
camera::Camera2D,
color::Color,
math::Vector2,
prelude::{RaylibDraw, RaylibMode2D},
texture::Texture2D,
RaylibHandle,
};
use super::non_ref_raylib::HackedRaylibHandle;
#[derive(Debug)]
pub struct WorldPaintTexture {
texture: Texture2D,
}
impl WorldPaintTexture {
/// Construct a new world paint texture
pub fn new(texture: Texture2D) -> Self {
Self { texture }
}
pub fn render(
&self,
raylib: &mut RaylibMode2D<'_, HackedRaylibHandle>,
origin: Vector2,
camera: &Camera2D,
) {
// Convert the screen edges to world space
let top_left = raylib.get_screen_to_world2D(Vector2::new(0.0, 0.0), camera);
let bottom_right = raylib.get_screen_to_world2D(raylib.get_screen_size(), camera);
// Calculate the distance between the edges and the origin
let left_edge_distance = top_left.x - origin.x;
let right_edge_distance = bottom_right.x - origin.x;
// Calculate the x position to draw the tile in order for there always to be a tile covering the edges
let left_tile_x =
(left_edge_distance / self.texture.width as f32).floor() * self.texture.width as f32;
let right_tile_x =
left_tile_x + self.texture.width as f32;
// Render the tiles
raylib.draw_texture_v(
&self.texture,
Vector2::new(left_tile_x, origin.y),
Color::WHITE,
);
raylib.draw_texture_v(
&self.texture,
Vector2::new(right_tile_x, origin.y),
Color::WHITE,
);
}
}

View File

@ -7,7 +7,7 @@ async fn main() {
// Load the general config for the game // Load the general config for the game
// This happens here so we can properly track sentry events // This happens here so we can properly track sentry events
let game_config = GameConfig::load( let mut game_config = GameConfig::load(
StaticGameData::get("configs/application.json").expect("Failed to load application.json"), StaticGameData::get("configs/application.json").expect("Failed to load application.json"),
).unwrap(); ).unwrap();
@ -22,5 +22,5 @@ async fn main() {
)); ));
// Start the game // Start the game
game_begin(&game_config).await.unwrap(); game_begin(&mut game_config).await.unwrap();
} }