diff --git a/.gitmodules b/.gitmodules index 9a8c6484..36c2f7aa 100644 --- a/.gitmodules +++ b/.gitmodules @@ -6,3 +6,6 @@ path = third_party/sm url = https://github.com/Ewpratten/sm ignore = dirty +[submodule "third_party/rs-tiled"] + path = third_party/rs-tiled + url = https://github.com/Ewpratten/rs-tiled diff --git a/assets/ewpratten/env_beachTile/env_beachTile.xcf b/assets/ewpratten/env_beachTile/env_beachTile.xcf new file mode 100644 index 00000000..6f7bfc92 Binary files /dev/null and b/assets/ewpratten/env_beachTile/env_beachTile.xcf differ diff --git a/assets/ewpratten/env_beachTile/env_beachTileSwirly.xcf b/assets/ewpratten/env_beachTile/env_beachTileSwirly.xcf new file mode 100644 index 00000000..448b9929 Binary files /dev/null and b/assets/ewpratten/env_beachTile/env_beachTileSwirly.xcf differ diff --git a/game.tiled-project b/game.tiled-project new file mode 100644 index 00000000..05405582 --- /dev/null +++ b/game.tiled-project @@ -0,0 +1,12 @@ +{ + "automappingRulesFile": "", + "commands": [ + ], + "extensionsPath": "extensions", + "folders": [ + "game/dist" + ], + "objectTypesFile": "", + "propertyTypes": [ + ] +} diff --git a/game.tiled-session b/game.tiled-session new file mode 100644 index 00000000..9095aa56 --- /dev/null +++ b/game.tiled-session @@ -0,0 +1,65 @@ +{ + "Map/SizeTest": { + "height": 4300, + "width": 2 + }, + "activeFile": "", + "expandedProjectPaths": [ + ], + "file.lastUsedOpenFilter": "All Files (*)", + "fileStates": { + "": { + "scaleInDock": 1 + }, + "#env_beachTile": { + "dynamicWrapping": false + }, + "game/dist/assets/env/env_beachTile/beachTile.tsx": { + "dynamicWrapping": true, + "scaleInDock": 1, + "scaleInEditor": 1 + }, + "game/dist/assets/env/env_beachTile/beachTileSwirly.tsx": { + "dynamicWrapping": true, + "scaleInDock": 1, + "scaleInEditor": 1 + }, + "game/dist/map/map_gameMap.tmx": { + "scale": 0.33, + "selectedLayer": 0, + "viewCenter": { + "x": 200, + "y": -148.4848484848485 + } + }, + "game/dist/map_gameMap.tmx": { + "scale": 0.25, + "selectedLayer": 0, + "viewCenter": { + "x": 224, + "y": -80 + } + }, + "game/dist/map_gameMap.tmx#env_beachTile": { + "dynamicWrapping": false + } + }, + "last.imagePath": "/home/ewpratten/projects/ludum-dare-50/game/dist/assets/env/env_beachTile", + "map.fixedSize": false, + "map.lastUsedFormat": "tmx", + "map.tileHeight": 128, + "map.tileWidth": 128, + "openFiles": [ + ], + "project": "game.tiled-project", + "property.type": "float", + "recentFiles": [ + "game/dist/assets/env/env_beachTile/beachTile.tsx", + "game/dist/map_gameMap.tmx", + "game/dist/assets/env/env_beachTile/beachTileSwirly.tsx", + "game/dist/map/map_gameMap.tmx" + ], + "tileset.embedInMap": true, + "tileset.lastUsedFormat": "tsx", + "tileset.type": 1 +} diff --git a/game/dist/.gitignore b/game/dist/.gitignore new file mode 100644 index 00000000..a06c1e50 --- /dev/null +++ b/game/dist/.gitignore @@ -0,0 +1 @@ +!**/env/ \ No newline at end of file diff --git a/game/dist/assets/env/env_beachTile/env_beachTile.png b/game/dist/assets/env/env_beachTile/env_beachTile.png new file mode 100644 index 00000000..fafdd41a Binary files /dev/null and b/game/dist/assets/env/env_beachTile/env_beachTile.png differ diff --git a/game/dist/assets/env/env_beachTile/env_beachTileSwirly.png b/game/dist/assets/env/env_beachTile/env_beachTileSwirly.png new file mode 100644 index 00000000..d4563aa5 Binary files /dev/null and b/game/dist/assets/env/env_beachTile/env_beachTileSwirly.png differ diff --git a/game/dist/map_gameMap.tmx b/game/dist/map_gameMap.tmx new file mode 100644 index 00000000..f3384c58 --- /dev/null +++ b/game/dist/map_gameMap.tmx @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,2,2,1,1,2,2,0,0,0,0, +0,0,0,0,0,0,2,2,1,1,2,2,0,0,0,0, +0,0,0,0,0,0,2,1,0,0,1,2,0,0,0,0, +0,0,0,0,0,0,2,2,1,1,2,2,0,0,0,0, +0,0,0,0,0,0,2,2,1,1,2,2,0,0,0,0, +0,0,0,0,0,0,1,1,2,2,1,1,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + + + + diff --git a/game/game_logic/Cargo.toml b/game/game_logic/Cargo.toml index be0d2754..e285ec99 100644 --- a/game/game_logic/Cargo.toml +++ b/game/game_logic/Cargo.toml @@ -25,4 +25,5 @@ thiserror = "1.0.30" approx = "0.5.1" poll-promise = { version = "0.1.0", features = ["tokio"] } tempfile = "3.3.0" -nalgebra = "0.30.1" \ No newline at end of file +nalgebra = "0.30.1" +tiled = { version = "0.10.1", path = "../../third_party/rs-tiled" } diff --git a/game/game_logic/src/coord_convert.rs b/game/game_logic/src/coord_convert.rs new file mode 100644 index 00000000..92d4d9a4 --- /dev/null +++ b/game/game_logic/src/coord_convert.rs @@ -0,0 +1,11 @@ +use nalgebra as na; + +/// Converts from the tiled coordinate system to the game coordinate system. +pub fn tiled_to_game(vec: na::Vector2) -> na::Vector2 { + na::Vector2::new(vec.x, vec.y * -1.0) +} + +/// Converts from the game coordinate system to the tiled coordinate system. +pub fn game_to_tiled(vec: na::Vector2) -> na::Vector2 { + tiled_to_game(vec) +} \ No newline at end of file diff --git a/game/game_logic/src/lib.rs b/game/game_logic/src/lib.rs index 7744fcec..cd1534ce 100644 --- a/game/game_logic/src/lib.rs +++ b/game/game_logic/src/lib.rs @@ -40,6 +40,7 @@ pub(crate) mod project_constants; pub(crate) mod rendering; pub(crate) mod scenes; pub(crate) mod model; +pub(crate) mod coord_convert; /// This is the game logic entrypoint. Despite being async, /// this is expected to block the main thread for rendering and stuff. @@ -74,6 +75,7 @@ pub async fn entrypoint(force_recreate_savefiles: bool) { |builder| { builder .msaa_4x() + .resizable() // .vsync() .title(project_constants.game_name.as_str()) .height(project_constants.base_window_size.1 as i32) diff --git a/game/game_logic/src/rendering/utilities/map_render.rs b/game/game_logic/src/rendering/utilities/map_render.rs new file mode 100644 index 00000000..b9822917 --- /dev/null +++ b/game/game_logic/src/rendering/utilities/map_render.rs @@ -0,0 +1,213 @@ +use std::{collections::HashMap, path::PathBuf, sync::Arc}; + +use crate::asset_manager::{load_texture_from_internal_data, InternalData}; +use nalgebra as na; +use raylib::{ + camera::Camera2D, + color::Color, + math::Vector2, + prelude::{RaylibDraw, RaylibDrawHandle, RaylibMode2D}, + texture::Texture2D, + RaylibHandle, RaylibThread, +}; +use tiled::{Loader, Map, ResourceCache, ResourcePath, ResourcePathBuf, Tileset}; + +/// Possible errors generated by the map loading process +#[derive(Debug, thiserror::Error)] +pub enum MapRenderError { + #[error("Could not load embedded asset: {0}")] + AssetNotFound(String), + #[error(transparent)] + TiledError(#[from] tiled::Error), +} + +#[derive(Debug)] +struct ProgramDataTileCache { + tilesets: HashMap>, + internal_loader: Loader, +} + +impl ProgramDataTileCache { + fn new() -> Self { + Self { + tilesets: HashMap::new(), + internal_loader: Loader::new(), + } + } +} + +impl ResourceCache for ProgramDataTileCache { + /// Load the tileset. First attempts to pull from an in-RAM cache, otherwise attempts to load from disk. + fn get_tileset(&self, path: impl AsRef) -> Option> { + let possibly_cached_tileset = self.tilesets.get(path.as_ref()).map(Clone::clone); + if let Some(tileset) = possibly_cached_tileset { + return Some(tileset); + } else { + // Pull the TSX from storage and parse it + InternalData::get(path.as_ref().to_str().unwrap()).map(|file| { + let data = file.data.into_owned(); + Arc::new( + self.internal_loader + .load_tsx_tileset_from(data.as_slice(), path) + .unwrap(), + ) + }) + } + } + + fn get_or_try_insert_tileset_with( + &mut self, + path: ResourcePathBuf, + f: F, + ) -> Result, E> + where + F: FnOnce() -> Result, + { + Ok(match self.tilesets.entry(path) { + std::collections::hash_map::Entry::Occupied(o) => o.into_mut(), + std::collections::hash_map::Entry::Vacant(v) => v.insert(Arc::new(f()?)), + } + .clone()) + } +} + +#[derive(Debug)] +pub struct MapRenderer { + map: Map, + tile_textures: HashMap, +} + +impl MapRenderer { + /// Construct a new MapRenderer. + pub fn new( + tmx_path: &str, + raylib: &mut RaylibHandle, + raylib_thread: &RaylibThread, + ) -> Result { + // Pull the TMX from storage + let data = InternalData::get(tmx_path) + .ok_or(MapRenderError::AssetNotFound(tmx_path.to_string()))? + .data + .into_owned(); + + // Attempt to parse the TMX file + let mut loader = Loader::with_cache(ProgramDataTileCache::new()); + let map = loader.load_tmx_map_from(data.as_slice(), tmx_path)?; + + // Iterate over all images in the map + let mut tile_textures = HashMap::new(); + for tileset in map.tilesets() { + for (idx, tile) in tileset.tiles() { + if let Some(image) = &tile.data.image { + // We now have a path to an image + let image_path = image.source.clone(); + + // Load the texture + let texture = load_texture_from_internal_data( + raylib, + raylib_thread, + image_path.to_str().unwrap(), + ) + .unwrap(); + + // Store the texture in the cache + tile_textures.insert(image_path, texture); + } + } + } + + Ok(Self { map, tile_textures }) + } + + pub fn sample_friction_at(&self, position: na::Vector2) -> f32 { + todo!() + } + + pub fn sample_temperature_at(&self, position: na::Vector2) -> f32 { + todo!() + } + + pub fn render_map(&self, draw_handle: &mut RaylibMode2D, camera: &Camera2D, show_debug_grid:bool) { + // Get the window corners in world space + let screen_width = draw_handle.get_screen_width(); + let screen_height = draw_handle.get_screen_height(); + let world_win_top_left = draw_handle.get_screen_to_world2D(Vector2::new(0.0, 0.0), camera); + let world_win_bottom_right = draw_handle.get_screen_to_world2D( + Vector2::new(screen_width as f32, screen_height as f32), + camera, + ); + + // Handle each layer from the bottom up + for layer in self.map.layers() { + // Handle different layer types + match layer.layer_type() { + tiled::LayerType::TileLayer(layer) => { + // Keep track of our sampler X and Y values + let mut sampler_x = 0; + let mut sampler_y = 0; + + // Get the tile width and height + let tile_width = 128; + let tile_height = 128; + + // Loop until we have covered all tiles on the screen + for y in (world_win_top_left.y as i64)..(world_win_bottom_right.y as i64) { + // Convert the pixel coordinates to tile coordinates + let tile_y = (y as f32 / tile_height as f32).floor() as i32; + + // If we are looking at a new tile, update the sampler + if sampler_y != tile_y { + sampler_y = tile_y; + + for x in + (world_win_top_left.x as i64)..(world_win_bottom_right.x as i64) + { + // Convert the pixel coordinates to tile coordinates + let tile_x = (x as f32 / tile_width as f32).floor() as i32; + // debug!("Tile: ({}, {})", tile_x, tile_y); + + // If we are looking at a new tile, update the sampler + if sampler_x != tile_x { + sampler_x = tile_x; + + // Get the tile at this coordinate + if let Some(tile) = layer.get_tile(sampler_x, sampler_y) { + // debug!("Tile: ({}, {})", tile_x, tile_y); + // Fetch the texture for this tile + let real_tile = tile.get_tile().unwrap(); + let texture = self + .tile_textures + .get(&real_tile.image.as_ref().unwrap().source) + .unwrap(); + + // Draw the tile + draw_handle.draw_texture( + texture, + tile_x * tile_width as i32, + tile_y * tile_height as i32, + Color::WHITE, + ); + } + + if show_debug_grid { + draw_handle.draw_rectangle_lines( + tile_x * tile_width as i32, + tile_y * tile_height as i32, + self.map.tile_width as i32, + self.map.tile_height as i32, + Color::RED, + ); + draw_handle.draw_pixel(x as i32, y as i32, Color::BLUE); + } + } + } + } + } + } + tiled::LayerType::ObjectLayer(_) => todo!(), + tiled::LayerType::ImageLayer(_) => todo!(), + tiled::LayerType::GroupLayer(_) => todo!(), + } + } + } +} diff --git a/game/game_logic/src/rendering/utilities/mod.rs b/game/game_logic/src/rendering/utilities/mod.rs index 09d38ecf..524ae38e 100644 --- a/game/game_logic/src/rendering/utilities/mod.rs +++ b/game/game_logic/src/rendering/utilities/mod.rs @@ -1 +1,2 @@ -pub mod anim_texture; \ No newline at end of file +pub mod anim_texture; +pub mod map_render; \ No newline at end of file diff --git a/game/game_logic/src/scenes/mod.rs b/game/game_logic/src/scenes/mod.rs index ee1a40c8..e0b4a9d8 100644 --- a/game/game_logic/src/scenes/mod.rs +++ b/game/game_logic/src/scenes/mod.rs @@ -65,8 +65,13 @@ impl SceneRenderDelegate { // Render the main menu if in it, otherwise, render the game match self.menu_control_signal { MenuStateSignal::StartGame => { - self.scene_playable - .render_frame(raylib, rl_thread, &discord, global_resources, constants) + // self.scene_playable + // .render_frame(raylib, rl_thread, &discord, global_resources, constants) + // .await; + + // TODO: remove this test scene + self.scene_test_fox + .render_frame(raylib, rl_thread, &discord, global_resources) .await; } MenuStateSignal::QuitGame => unimplemented!(), @@ -81,19 +86,25 @@ impl SceneRenderDelegate { .scene_main_menu .render_options_frame(raylib, rl_thread, discord, global_resources, constants) .await - }, + } MenuStateSignal::DoCredits => { self.menu_control_signal = self .scene_main_menu .render_credits_frame(raylib, rl_thread, discord, global_resources, constants) .await - }, + } MenuStateSignal::DoLeaderboard => { self.menu_control_signal = self .scene_main_menu - .render_leaderboard_frame(raylib, rl_thread, discord, global_resources, constants) + .render_leaderboard_frame( + raylib, + rl_thread, + discord, + global_resources, + constants, + ) .await - }, + } } } } diff --git a/game/game_logic/src/scenes/test_fox.rs b/game/game_logic/src/scenes/test_fox.rs index 526dc57e..d9d1973e 100644 --- a/game/game_logic/src/scenes/test_fox.rs +++ b/game/game_logic/src/scenes/test_fox.rs @@ -1,17 +1,20 @@ //! This "scene" is used only for testing animation and resource loading //! It should be removed once the game is being worked on -use raylib::prelude::*; use nalgebra as na; +use raylib::prelude::*; use crate::{ - discord::DiscordChannel, global_resource_package::GlobalResources, - rendering::utilities::anim_texture::AnimatedTexture, + discord::DiscordChannel, + global_resource_package::GlobalResources, + rendering::utilities::{anim_texture::AnimatedTexture, map_render::MapRenderer}, }; #[derive(Debug)] pub struct TestFoxScene { fox_animation: AnimatedTexture, + world_map: MapRenderer, + camera: Camera2D, } impl TestFoxScene { @@ -20,11 +23,29 @@ impl TestFoxScene { // Load the fox texture let fox = AnimatedTexture::new(raylib_handle, thread, "chr", "testFox").unwrap(); - Self { fox_animation: fox } + // Load the map + let map_renderer = MapRenderer::new("map_gameMap.tmx", raylib_handle, thread).unwrap(); + + // Create a camera + let camera = Camera2D { + target: Vector2 { x: 0.0, y: 0.0 }, + offset: Vector2 { + x: raylib_handle.get_screen_width() as f32, + y: (raylib_handle.get_screen_height() as f32) * -0.5, + }, + rotation: 0.0, + zoom: 1.0, + }; + + Self { + fox_animation: fox, + world_map: map_renderer, + camera, + } } /// Handler for each frame - pub fn render_frame( + pub async fn render_frame( &mut self, raylib: &mut RaylibHandle, rl_thread: &RaylibThread, @@ -46,5 +67,27 @@ impl TestFoxScene { None, None, ); + + // Allow the camera to be moved with wasd + if draw.is_key_down(KeyboardKey::KEY_W) { + self.camera.target.y -= 5.0; + } + if draw.is_key_down(KeyboardKey::KEY_S) { + self.camera.target.y += 5.0; + } + if draw.is_key_down(KeyboardKey::KEY_A) { + self.camera.target.x -= 5.0; + } + if draw.is_key_down(KeyboardKey::KEY_D) { + self.camera.target.x += 5.0; + } + + { + // Begin camera mode + let mut ctx2d = draw.begin_mode2D(self.camera); + + // Render the map + self.world_map.render_map(&mut ctx2d, &self.camera, true); + } } } diff --git a/third_party/rs-tiled b/third_party/rs-tiled new file mode 160000 index 00000000..1629541a --- /dev/null +++ b/third_party/rs-tiled @@ -0,0 +1 @@ +Subproject commit 1629541a446cff53c4dd6aa827aa1bb0afc4e96c