Merge branch 'master' into main_menu_screen

This commit is contained in:
Evan Pratten 2021-10-02 15:56:55 -07:00 committed by GitHub
commit c7ee45d0b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 669 additions and 73 deletions

9
.vscode/tasks.json vendored
View File

@ -45,6 +45,15 @@
"env": {
"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)
[![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)

View File

@ -32,6 +32,7 @@ cfg-if = "1.0"
num-derive = "0.3"
num = "0.4"
tiled = { version ="0.9.5", default-features = false }
async-trait = "0.1.51"
[dev-dependencies]
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": [
1080,
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 crate::scenes::ingame_scene::world::WORLD_LEVEL_X_OFFSET;
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<(), ()> {
// Convert the player to a rectangle
pub fn modify_player_based_on_forces(
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;
// Calculate a bounding rect around the player both now, and one frame in the future
let player_rect = Rectangle::new(
predicted_player_position.x - (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
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 (floor_rect.check_collision_recs(&player_rect) || player_rect.y + player_rect.height > floor_rect.y)
&& player.velocity.y > 0.0
if (check_player_colliding_with_floor()
|| check_player_colliding_with_floor_next_frame()
|| check_player_colliding_with_colliders())
&& player.velocity.y != 0.0
{
player.velocity.y = 0.0;
// Handle ending a jump
if player.current_state == CharacterState::Jumping {
player.set_state(CharacterState::Running);
if player.current_state == CharacterState::Jumping
|| 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;
// Apply gravity
player.velocity.y += GRAVITY_PPS;
Ok(())
}

View File

@ -2,13 +2,16 @@ pub mod collisions;
pub mod render;
use chrono::{DateTime, Utc};
use raylib::{math::Vector2, texture::Texture2D};
use raylib::{
math::{Rectangle, Vector2},
texture::Texture2D,
};
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 {
#[default]
Running,
@ -19,6 +22,8 @@ pub enum CharacterState {
#[derive(Debug)]
pub struct MainCharacter {
pub position: Vector2,
pub movement_force: Vector2,
pub base_velocity: Vector2,
pub velocity: Vector2,
pub size: Vector2,
pub sprite_sheet: AnimatedSpriteSheet,
@ -30,8 +35,10 @@ impl MainCharacter {
pub fn new(position: Vector2, sprite_sheet: Texture2D) -> Self {
Self {
position,
velocity: Vector2::new(20.0, 0.0),
size: Vector2::new(100.0, 130.0),
movement_force: Vector2::zero(),
velocity: Vector2::zero(),
base_velocity: Vector2::new(0.0, GRAVITY_PPS),
size: Vector2::new(100.0, 100.0),
sprite_sheet: AnimatedSpriteSheet::new(
sprite_sheet,
Vector2::new(300.0, 300.0),
@ -44,20 +51,28 @@ impl MainCharacter {
}
}
pub fn apply_force(&mut self, force: Vector2) -> Option<()> {
self.velocity = force;
modify_player_based_on_forces(self).unwrap();
Some(())
}
pub fn update_gravity(&mut self) {
modify_player_based_on_forces(self).unwrap();
}
pub fn set_state(&mut self, state: CharacterState) {
pub fn update_player(
&mut self,
state: Option<CharacterState>,
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;
self.current_state = state.clone();
self.state_set_timestamp = Utc::now();
}
// Handle extra external forces based on the character state
self.movement_force = match state {
CharacterState::Running => Vector2::new(12.0, 0.0),
CharacterState::Jumping => Vector2::new(12.0, -30.0),
CharacterState::Dashing => Vector2::new(30.0, -20.0),
};
}
// 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 raylib::prelude::*;
@ -37,4 +37,16 @@ pub fn render_character_in_camera_space(
Some(Vector2::new(player.size.y, player.size.y)),
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};
@ -6,7 +8,8 @@ use crate::{GameConfig, utilities::non_ref_raylib::HackedRaylibHandle};
#[derive(Debug)]
pub struct GameContext {
pub renderer: RefCell<HackedRaylibHandle>,
pub config: GameConfig
pub config: GameConfig,
pub discord_rpc_send: Sender<Option<ActivityBuilder>>
}
// impl GameContext {

View File

@ -1,6 +1,7 @@
#![feature(derive_default_enum)]
#![feature(custom_inner_attributes)]
#![feature(stmt_expr_attributes)]
#![feature(async_await)]
#![feature(c_variadic)]
#![deny(unsafe_code)]
#![warn(
@ -69,7 +70,7 @@
)]
#![clippy::msrv = "1.57.0"]
use std::cell::RefCell;
use std::{cell::RefCell, sync::mpsc::TryRecvError};
use discord_sdk::activity::ActivityBuilder;
use raylib::prelude::*;
@ -97,6 +98,8 @@ extern crate serde;
extern crate approx;
#[macro_use]
extern crate num_derive;
#[macro_use]
extern crate async_trait;
mod context;
mod discord_rpc;
@ -106,7 +109,7 @@ pub use utilities::{datastore::StaticGameData, game_config::GameConfig};
mod character;
/// 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
#[cfg(debug_assertions)]
let _puffin_server =
@ -133,11 +136,14 @@ pub async fn game_begin(game_config: &GameConfig) -> Result<(), Box<dyn std::err
};
maybe_set_discord_presence(
&discord_rpc,
ActivityBuilder::default().details("Testing..."),
ActivityBuilder::default().details("Game starting"),
)
.await
.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 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.1,
)
.title(&game_config.name)
.title(&format!("[{}]", game_config.name))
.vsync()
.msaa_4x()
.resizable()
@ -161,6 +167,7 @@ pub async fn game_begin(game_config: &GameConfig) -> Result<(), Box<dyn std::err
context = Box::new(GameContext {
renderer: RefCell::new(rl.into()),
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,
)?;
while !context.renderer.borrow().window_should_close() {
// Profile the main game 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)
.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)
#[allow(unsafe_code)]
unsafe {
@ -260,6 +287,22 @@ pub async fn game_begin(game_config: &GameConfig) -> Result<(), Box<dyn std::err
unsafe {
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(())
}

View File

@ -1,6 +1,7 @@
use dirty_fsm::{Action, ActionFlag};
use discord_sdk::activity::{ActivityBuilder, Assets};
use raylib::{color::Color, prelude::RaylibDraw};
use tracing::{debug, trace};
use tracing::{debug, error, trace};
use crate::{
context::GameContext,
@ -26,8 +27,20 @@ impl Action<Scenes, ScreenError, GameContext> for FsmErrorScreen {
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");
// 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(())
}

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 discord_sdk::activity::{ActivityBuilder, Assets};
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 tracing::{debug, trace};
use tracing::{debug, error, trace};
mod hud;
pub mod level;
mod update;
mod world;
pub mod world;
#[derive(Debug)]
pub struct InGameScreen {
camera: Camera2D,
player: MainCharacter,
world_background: WorldPaintTexture,
levels: Vec<Level>,
current_level_idx: usize,
}
impl 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 {
camera: Camera2D {
offset: Vector2::zero(),
@ -26,7 +44,10 @@ impl InGameScreen {
rotation: 0.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(())
}
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");
// 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(())
}
@ -75,8 +110,12 @@ impl Action<Scenes, ScreenError, GameContext> for InGameScreen {
// Render the HUD
self.render_screen_space(&mut renderer, &context.config);
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> {
debug!("Finished InGameScreen");

View File

@ -17,26 +17,37 @@ impl FrameUpdate for InGameScreen {
config: &GameConfig,
) {
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
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);
// Check the only possible keyboard inputs
let is_jump = raylib.is_key_pressed(KeyboardKey::KEY_SPACE);
let is_dash = raylib.is_key_pressed(KeyboardKey::KEY_LEFT_SHIFT);
let is_pause = raylib.is_key_pressed(KeyboardKey::KEY_ESCAPE);
let is_jump = raylib.is_key_pressed(KeyboardKey::KEY_SPACE)
&& !(self.player.current_state == CharacterState::Jumping);
let is_dash = raylib.is_key_pressed(KeyboardKey::KEY_LEFT_SHIFT)
&& !(self.player.current_state == CharacterState::Dashing);
if is_jump {
self.player.apply_force(Vector2::new(0.0, -30.0));
self.player.set_state(CharacterState::Jumping);
self.player.update_player(Some(CharacterState::Jumping), &cur_level.colliders,
-cur_level.platform_tex.height as f32,);
} else if is_dash {
self.player.apply_force(Vector2::new(40.0, -10.0));
self.player.set_state(CharacterState::Dashing);
self.player.update_player(Some(CharacterState::Dashing), &cur_level.colliders,
-cur_level.platform_tex.height as f32,);
} else {
if self.player.current_state != CharacterState::Jumping {
self.player.set_state(CharacterState::Running);
if self.player.current_state != CharacterState::Jumping
&& 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::*;
pub const WORLD_LEVEL_X_OFFSET: f32 = 200.0;
impl WorldSpaceRender for InGameScreen {
fn render_world_space(
&mut self,
@ -15,8 +17,15 @@ impl WorldSpaceRender for InGameScreen {
config: &GameConfig,
) {
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
let screen_world_zero = raylib.get_screen_to_world2D(Vector2::zero(), self.camera);
@ -30,5 +39,9 @@ impl WorldSpaceRender for InGameScreen {
5,
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 chrono::{DateTime, Utc};
use dirty_fsm::{Action, ActionFlag};
use discord_sdk::activity::{ActivityBuilder, Assets};
use raylib::prelude::*;
use crate::{GameConfig, context::GameContext, utilities::{
@ -13,7 +14,7 @@ use crate::{GameConfig, context::GameContext, utilities::{
}};
use super::{Scenes, ScreenError};
use tracing::{debug, info, trace};
use tracing::{debug, info, error, trace};
/// Defines how long the loading screen should be displayed.
const LOADING_SCREEN_DURATION_SECONDS: u8 = 3;
@ -49,9 +50,18 @@ impl Action<Scenes, ScreenError, GameContext> for LoadingScreen {
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");
// 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
self.start_timestamp = Some(Utc::now());

View File

@ -2,19 +2,24 @@ 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::{GameConfig, context::GameContext, utilities::{
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, info, trace};
use tracing::{debug, error, info, trace};
#[derive(Debug)]
pub struct MainMenuScreen {
@ -42,9 +47,18 @@ impl Action<Scenes, ScreenError, GameContext> for MainMenuScreen {
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");
// 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(())
}
@ -83,7 +97,7 @@ impl ScreenSpaceRender for MainMenuScreen {
fn render_screen_space(
&mut self,
raylib: &mut crate::utilities::non_ref_raylib::HackedRaylibHandle,
config: &GameConfig
config: &GameConfig,
) {
// Render the background
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::{
context::GameContext,
utilities::{
@ -15,6 +21,7 @@ pub mod loading_screen;
pub mod main_menu_screen;
pub mod how_to_play_screen;
pub mod options_screen;
pub mod pause_screen;
/// Defines all scenes
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
@ -26,6 +33,7 @@ pub enum Scenes {
InGameScene,
HowToPlayScreen,
OptionsScreen,
PauseScreen,
}
/// 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
let player_sprite_sheet =
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
let mut machine = StateMachine::new();
@ -56,9 +67,13 @@ pub fn build_screen_state_machine(
LoadingScreen::new(raylib_handle, thread)?,
)?;
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::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)
}

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(
&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)]
pub enum ResourceLoadError {
#[error(transparent)]
JsonDeser(#[from] serde_json::Error),
#[error(transparent)]
Io(#[from] std::io::Error),
#[error("Could not load embedded asset: {0}")]

View File

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

View File

@ -1,3 +1,4 @@
pub mod anim_render;
pub mod datastore;
pub mod discord;
pub mod game_config;
@ -6,4 +7,4 @@ pub mod math;
pub mod non_ref_raylib;
pub mod render_layer;
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
// 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"),
).unwrap();
@ -22,5 +22,5 @@ async fn main() {
));
// Start the game
game_begin(&game_config).await.unwrap();
game_begin(&mut game_config).await.unwrap();
}