diff --git a/.woodpecker/html.yml b/.woodpecker/html.yml
index cc7e6b7..9621834 100644
--- a/.woodpecker/html.yml
+++ b/.woodpecker/html.yml
@@ -7,11 +7,16 @@ steps:
secrets: [access_key, secret_key]
commands:
- mc alias set minio https://minio.ragarock.moe $access_key $secret_key
- - mc cp -quiet --recursive minio/yuno/cache-html/target target
+ - mc cp -quiet --recursive minio/yuno/cache-html/target/ ci-cache/
failure: ignore
build:
image: git.ragarock.moe/silvana/yuno/rust:latest
- environment: [CARGO_TERM_COLOR=always, CARGO_HOME=./.cargo-home]
+ environment:
+ [
+ CARGO_TERM_COLOR=always,
+ CARGO_HOME=./.cargo-home,
+ CARGO_TARGET_DIR=./ci-cache,
+ ]
commands:
- rustup default nightly
- rustup target add wasm32-unknown-unknown
@@ -27,5 +32,5 @@ steps:
secrets: [access_key, secret_key]
commands:
- mc alias set minio https://minio.ragarock.moe $access_key $secret_key
- - mc cp -quiet --recursive target/ minio/yuno/cache-html/target
+ - mc cp -quiet --recursive ci-cache/ minio/yuno/cache-html/target/
failure: ignore
diff --git a/.woodpecker/quality.yml b/.woodpecker/quality.yml
index df912d2..d6cadbc 100644
--- a/.woodpecker/quality.yml
+++ b/.woodpecker/quality.yml
@@ -7,25 +7,40 @@ steps:
secrets: [access_key, secret_key]
commands:
- mc alias set minio https://minio.ragarock.moe $access_key $secret_key
- - mc cp -quiet --recursive minio/yuno/cache-amd64/target target
+ - mc cp -quiet --recursive minio/yuno/cache-amd64/target/ ci-cache/
failure: ignore
fmt:
image: git.ragarock.moe/silvana/yuno/rust:latest
- environment: [CARGO_TERM_COLOR=always, CARGO_HOME=./.cargo-home]
+ environment:
+ [
+ CARGO_TERM_COLOR=always,
+ CARGO_HOME=./.cargo-home,
+ CARGO_TARGET_DIR=./ci-cache,
+ ]
commands:
- rustup default nightly
- rustup component add rustfmt
- cargo fmt -- --check
clippy:
image: git.ragarock.moe/silvana/yuno/rust:latest
- environment: [CARGO_TERM_COLOR=always, CARGO_HOME=./.cargo-home]
+ environment:
+ [
+ CARGO_TERM_COLOR=always,
+ CARGO_HOME=./.cargo-home,
+ CARGO_TARGET_DIR=./ci-cache,
+ ]
commands:
- rustup default nightly
- rustup component add clippy
- cargo clippy -- -D warnings
test:
image: git.ragarock.moe/silvana/yuno/rust:latest
- environment: [CARGO_TERM_COLOR=always, CARGO_HOME=./.cargo-home]
+ environment:
+ [
+ CARGO_TERM_COLOR=always,
+ CARGO_HOME=./.cargo-home,
+ CARGO_TARGET_DIR=./ci-cache,
+ ]
commands:
- rustup default nightly
- cargo check
@@ -35,5 +50,5 @@ steps:
secrets: [access_key, secret_key]
commands:
- mc alias set minio https://minio.ragarock.moe $access_key $secret_key
- - mc cp -quiet --recursive target/ minio/yuno/cache-amd64/target
+ - mc cp -quiet --recursive ci-cache/ minio/yuno/cache-amd64/target/
failure: ignore
diff --git a/Cargo.lock b/Cargo.lock
index 1599035..a9a050d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -566,6 +566,17 @@ dependencies = [
"syn 2.0.72",
]
+[[package]]
+name = "bevy_ecs_tilemap"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d880047f5deaf5166ffc08238125a4ccbd2837f781ca6525fa200fcf5785ba3b"
+dependencies = [
+ "bevy",
+ "log",
+ "regex",
+]
+
[[package]]
name = "bevy_encase_derive"
version = "0.14.1"
@@ -3560,6 +3571,18 @@ dependencies = [
"once_cell",
]
+[[package]]
+name = "tiled"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad408d366c0e1e7e4e504cabc14c77fdda77176d93b3c6abe5b3b31885df3ad0"
+dependencies = [
+ "base64 0.22.1",
+ "flate2",
+ "xml-rs",
+ "zstd",
+]
+
[[package]]
name = "tinyvec"
version = "1.8.0"
@@ -4514,9 +4537,12 @@ version = "0.1.0"
dependencies = [
"bevy",
"bevy_asset_loader",
+ "bevy_ecs_tilemap",
"bevy_kira_audio",
"image",
"log",
+ "thiserror",
+ "tiled",
"webbrowser",
"winit",
]
@@ -4541,3 +4567,31 @@ dependencies = [
"quote",
"syn 2.0.72",
]
+
+[[package]]
+name = "zstd"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9"
+dependencies = [
+ "zstd-safe",
+]
+
+[[package]]
+name = "zstd-safe"
+version = "7.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059"
+dependencies = [
+ "zstd-sys",
+]
+
+[[package]]
+name = "zstd-sys"
+version = "2.0.13+zstd.1.5.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa"
+dependencies = [
+ "cc",
+ "pkg-config",
+]
diff --git a/Cargo.toml b/Cargo.toml
index 6cbeaac..56f5725 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -70,3 +70,6 @@ log = { version = "0.4", features = [
"max_level_debug",
"release_max_level_warn",
] }
+thiserror = "1.0.63"
+bevy_ecs_tilemap = { version = "0.14.0" }
+tiled = { version = "0.12.0", features = ["wasm"] }
diff --git a/assets/maps/test.tmx b/assets/maps/test.tmx
new file mode 100644
index 0000000..639283f
--- /dev/null
+++ b/assets/maps/test.tmx
@@ -0,0 +1,30 @@
+
+
diff --git a/assets/textures/grass_tiles.png b/assets/textures/grass_tiles.png
new file mode 100644
index 0000000..7cf836e
--- /dev/null
+++ b/assets/textures/grass_tiles.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d90edc82dc2f5dba3642c2d4ed04f7093cd9161643b2e7bc350faf5d1abd9e49
+size 2118
diff --git a/src/lib.rs b/src/lib.rs
index 8696068..8d5be9a 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -5,9 +5,13 @@
#[cfg(debug_assertions)]
use bevy::diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin};
use bevy::prelude::*;
+use bevy_ecs_tilemap::TilemapPlugin;
+use map::MapPlugin;
use player::PlayerPlugin;
+mod loaders;
mod loading;
+mod map;
mod menu;
mod player;
@@ -25,8 +29,14 @@ pub struct YunoPlugin;
impl Plugin for YunoPlugin {
fn build(&self, app: &mut App) {
- app.init_state::()
- .add_plugins((LoadingPlugin, MenuPlugin, PlayerPlugin));
+ app.init_state::().add_plugins((
+ TilemapPlugin,
+ loaders::tiled::TiledMapPlugin,
+ MapPlugin,
+ LoadingPlugin,
+ MenuPlugin,
+ PlayerPlugin,
+ ));
#[cfg(debug_assertions)]
{
diff --git a/src/loaders/mod.rs b/src/loaders/mod.rs
new file mode 100644
index 0000000..6e4cf69
--- /dev/null
+++ b/src/loaders/mod.rs
@@ -0,0 +1 @@
+pub(crate) mod tiled;
diff --git a/src/loaders/tiled.rs b/src/loaders/tiled.rs
new file mode 100644
index 0000000..c98ff2a
--- /dev/null
+++ b/src/loaders/tiled.rs
@@ -0,0 +1,381 @@
+// How to use this:
+// You should copy/paste this into your project and use it much like examples/tiles.rs uses this
+// file. When you do so you will need to adjust the code based on whether you're using the
+// 'atlas` feature in bevy_ecs_tilemap. The bevy_ecs_tilemap uses this as an example of how to
+// use both single image tilesets and image collection tilesets. Since your project won't have
+// the 'atlas' feature defined in your Cargo config, the expressions prefixed by the #[cfg(...)]
+// macro will not compile in your project as-is. If your project depends on the bevy_ecs_tilemap
+// 'atlas' feature then move all of the expressions prefixed by #[cfg(not(feature = "atlas"))].
+// Otherwise remove all of the expressions prefixed by #[cfg(feature = "atlas")].
+//
+// Functional limitations:
+// * When the 'atlas' feature is enabled tilesets using a collection of images will be skipped.
+// * Only finite tile layers are loaded. Infinite tile layers and object layers will be skipped.
+
+use std::io::{Cursor, ErrorKind};
+use std::path::Path;
+use std::sync::Arc;
+
+use bevy::{
+ asset::{io::Reader, AssetLoader, AssetPath, AsyncReadExt},
+ log,
+ prelude::{
+ Added, Asset, AssetApp, AssetEvent, AssetId, Assets, Bundle, Commands, Component,
+ DespawnRecursiveExt, Entity, EventReader, GlobalTransform, Handle, Image, Plugin, Query,
+ Res, Transform, Update,
+ },
+ reflect::TypePath,
+ utils::HashMap,
+};
+use bevy_ecs_tilemap::prelude::*;
+
+use thiserror::Error;
+
+#[derive(Default)]
+pub struct TiledMapPlugin;
+
+impl Plugin for TiledMapPlugin {
+ fn build(&self, app: &mut bevy::prelude::App) {
+ app.init_asset::()
+ .register_asset_loader(TiledLoader)
+ .add_systems(Update, process_loaded_maps);
+ }
+}
+
+#[derive(TypePath, Asset)]
+pub struct TiledMap {
+ pub map: tiled::Map,
+
+ pub tilemap_textures: HashMap,
+
+ // The offset into the tileset_images for each tile id within each tileset.
+ pub tile_image_offsets: HashMap<(usize, tiled::TileId), u32>,
+}
+
+// Stores a list of tiled layers.
+#[derive(Component, Default)]
+pub struct TiledLayersStorage {
+ pub storage: HashMap,
+}
+
+#[derive(Default, Bundle)]
+pub struct TiledMapBundle {
+ pub tiled_map: Handle,
+ pub storage: TiledLayersStorage,
+ pub transform: Transform,
+ pub global_transform: GlobalTransform,
+ pub render_settings: TilemapRenderSettings,
+}
+
+struct BytesResourceReader {
+ bytes: Arc<[u8]>,
+}
+
+impl BytesResourceReader {
+ fn new(bytes: &[u8]) -> Self {
+ Self {
+ bytes: Arc::from(bytes),
+ }
+ }
+}
+
+impl tiled::ResourceReader for BytesResourceReader {
+ type Resource = Cursor>;
+ type Error = std::io::Error;
+
+ fn read_from(&mut self, _path: &Path) -> std::result::Result {
+ // In this case, the path is ignored because the byte data is already provided.
+ Ok(Cursor::new(self.bytes.clone()))
+ }
+}
+
+pub struct TiledLoader;
+
+#[derive(Debug, Error)]
+pub enum TiledAssetLoaderError {
+ /// An [IO](std::io) Error
+ #[error("Could not load Tiled file: {0}")]
+ Io(#[from] std::io::Error),
+}
+
+impl AssetLoader for TiledLoader {
+ type Asset = TiledMap;
+ type Settings = ();
+ type Error = TiledAssetLoaderError;
+
+ async fn load<'a>(
+ &'a self,
+ reader: &'a mut Reader<'_>,
+ _settings: &'a Self::Settings,
+ load_context: &'a mut bevy::asset::LoadContext<'_>,
+ ) -> Result {
+ let mut bytes = Vec::new();
+ reader.read_to_end(&mut bytes).await?;
+
+ let mut loader = tiled::Loader::with_cache_and_reader(
+ tiled::DefaultResourceCache::new(),
+ BytesResourceReader::new(&bytes),
+ );
+ let map = loader.load_tmx_map(load_context.path()).map_err(|e| {
+ std::io::Error::new(ErrorKind::Other, format!("Could not load TMX map: {e}"))
+ })?;
+
+ let mut tilemap_textures = HashMap::default();
+ let mut tile_image_offsets = HashMap::default();
+
+ for (tileset_index, tileset) in map.tilesets().iter().enumerate() {
+ let tilemap_texture = match &tileset.image {
+ None => {
+ {
+ let mut tile_images: Vec> = Vec::new();
+ for (tile_id, tile) in tileset.tiles() {
+ if let Some(img) = &tile.image {
+ // The load context path is the TMX file itself. If the file is at the root of the
+ // assets/ directory structure then the tmx_dir will be empty, which is fine.
+ let tmx_dir = load_context
+ .path()
+ .parent()
+ .expect("The asset load context was empty.");
+ let tile_path = tmx_dir.join(&img.source);
+ let asset_path = AssetPath::from(tile_path);
+ log::info!("Loading tile image from {asset_path:?} as image ({tileset_index}, {tile_id})");
+ let texture: Handle = load_context.load(asset_path.clone());
+ tile_image_offsets
+ .insert((tileset_index, tile_id), tile_images.len() as u32);
+ tile_images.push(texture.clone());
+ }
+ }
+
+ TilemapTexture::Vector(tile_images)
+ }
+ }
+ Some(img) => {
+ // The load context path is the TMX file itself. If the file is at the root of the
+ // assets/ directory structure then the tmx_dir will be empty, which is fine.
+ let texture: Handle = load_context.load(img.source.clone());
+
+ TilemapTexture::Single(texture.clone())
+ }
+ };
+
+ tilemap_textures.insert(tileset_index, tilemap_texture);
+ }
+
+ let asset_map = TiledMap {
+ map,
+ tilemap_textures,
+ tile_image_offsets,
+ };
+
+ log::info!("Loaded map: {}", load_context.path().display());
+ Ok(asset_map)
+ }
+
+ fn extensions(&self) -> &[&str] {
+ static EXTENSIONS: &[&str] = &["tmx"];
+ EXTENSIONS
+ }
+}
+
+pub fn process_loaded_maps(
+ mut commands: Commands,
+ mut map_events: EventReader>,
+ maps: Res>,
+ tile_storage_query: Query<(Entity, &TileStorage)>,
+ mut map_query: Query<(
+ &Handle,
+ &mut TiledLayersStorage,
+ &TilemapRenderSettings,
+ )>,
+ new_maps: Query<&Handle, Added>>,
+) {
+ let mut changed_maps = Vec::>::default();
+ for event in map_events.read() {
+ match event {
+ AssetEvent::Added { id } => {
+ log::info!("Map added!");
+ changed_maps.push(*id);
+ }
+ AssetEvent::Modified { id } => {
+ log::info!("Map changed!");
+ changed_maps.push(*id);
+ }
+ AssetEvent::Removed { id } => {
+ log::info!("Map removed!");
+ // if mesh was modified and removed in the same update, ignore the modification
+ // events are ordered so future modification events are ok
+ changed_maps.retain(|changed_handle| changed_handle == id);
+ }
+ _ => continue,
+ }
+ }
+
+ // If we have new map entities add them to the changed_maps list.
+ for new_map_handle in new_maps.iter() {
+ changed_maps.push(new_map_handle.id());
+ }
+
+ for changed_map in changed_maps.iter() {
+ for (map_handle, mut layer_storage, render_settings) in map_query.iter_mut() {
+ // only deal with currently changed map
+ if map_handle.id() != *changed_map {
+ continue;
+ }
+ if let Some(tiled_map) = maps.get(map_handle) {
+ // TODO: Create a RemoveMap component..
+ for layer_entity in layer_storage.storage.values() {
+ if let Ok((_, layer_tile_storage)) = tile_storage_query.get(*layer_entity) {
+ for tile in layer_tile_storage.iter().flatten() {
+ commands.entity(*tile).despawn_recursive()
+ }
+ }
+ // commands.entity(*layer_entity).despawn_recursive();
+ }
+
+ // The TilemapBundle requires that all tile images come exclusively from a single
+ // tiled texture or from a Vec of independent per-tile images. Furthermore, all of
+ // the per-tile images must be the same size. Since Tiled allows tiles of mixed
+ // tilesets on each layer and allows differently-sized tile images in each tileset,
+ // this means we need to load each combination of tileset and layer separately.
+ for (tileset_index, tileset) in tiled_map.map.tilesets().iter().enumerate() {
+ let Some(tilemap_texture) = tiled_map.tilemap_textures.get(&tileset_index)
+ else {
+ log::warn!("Skipped creating layer with missing tilemap textures.");
+ continue;
+ };
+
+ let tile_size = TilemapTileSize {
+ x: tileset.tile_width as f32,
+ y: tileset.tile_height as f32,
+ };
+
+ let tile_spacing = TilemapSpacing {
+ x: tileset.spacing as f32,
+ y: tileset.spacing as f32,
+ };
+
+ // Once materials have been created/added we need to then create the layers.
+ for (layer_index, layer) in tiled_map.map.layers().enumerate() {
+ let offset_x = layer.offset_x;
+ let offset_y = layer.offset_y;
+
+ let tiled::LayerType::Tiles(tile_layer) = layer.layer_type() else {
+ log::info!(
+ "Skipping layer {} because only tile layers are supported.",
+ layer.id()
+ );
+ continue;
+ };
+
+ let tiled::TileLayer::Finite(layer_data) = tile_layer else {
+ log::info!(
+ "Skipping layer {} because only finite layers are supported.",
+ layer.id()
+ );
+ continue;
+ };
+
+ let map_size = TilemapSize {
+ x: tiled_map.map.width,
+ y: tiled_map.map.height,
+ };
+
+ let grid_size = TilemapGridSize {
+ x: tiled_map.map.tile_width as f32,
+ y: tiled_map.map.tile_height as f32,
+ };
+
+ let map_type = match tiled_map.map.orientation {
+ tiled::Orientation::Hexagonal => {
+ TilemapType::Hexagon(HexCoordSystem::Row)
+ }
+ tiled::Orientation::Isometric => {
+ TilemapType::Isometric(IsoCoordSystem::Diamond)
+ }
+ tiled::Orientation::Staggered => {
+ TilemapType::Isometric(IsoCoordSystem::Staggered)
+ }
+ tiled::Orientation::Orthogonal => TilemapType::Square,
+ };
+
+ let mut tile_storage = TileStorage::empty(map_size);
+ let layer_entity = commands.spawn_empty().id();
+
+ for x in 0..map_size.x {
+ for y in 0..map_size.y {
+ // Transform TMX coords into bevy coords.
+ let mapped_y = tiled_map.map.height - 1 - y;
+
+ let mapped_x = x as i32;
+ let mapped_y = mapped_y as i32;
+
+ let layer_tile = match layer_data.get_tile(mapped_x, mapped_y) {
+ Some(t) => t,
+ None => {
+ continue;
+ }
+ };
+ if tileset_index != layer_tile.tileset_index() {
+ continue;
+ }
+ let layer_tile_data =
+ match layer_data.get_tile_data(mapped_x, mapped_y) {
+ Some(d) => d,
+ None => {
+ continue;
+ }
+ };
+
+ let texture_index = match tilemap_texture {
+ TilemapTexture::Single(_) => layer_tile.id(),
+ TilemapTexture::Vector(_) =>
+ *tiled_map.tile_image_offsets.get(&(tileset_index, layer_tile.id()))
+ .expect("The offset into to image vector should have been saved during the initial load."),
+ _ => unreachable!()
+ };
+
+ let tile_pos = TilePos { x, y };
+ let tile_entity = commands
+ .spawn(TileBundle {
+ position: tile_pos,
+ tilemap_id: TilemapId(layer_entity),
+ texture_index: TileTextureIndex(texture_index),
+ flip: TileFlip {
+ x: layer_tile_data.flip_h,
+ y: layer_tile_data.flip_v,
+ d: layer_tile_data.flip_d,
+ },
+ ..Default::default()
+ })
+ .id();
+ tile_storage.set(&tile_pos, tile_entity);
+ }
+ }
+
+ commands.entity(layer_entity).insert(TilemapBundle {
+ grid_size,
+ size: map_size,
+ storage: tile_storage,
+ texture: tilemap_texture.clone(),
+ tile_size,
+ spacing: tile_spacing,
+ transform: get_tilemap_center_transform(
+ &map_size,
+ &grid_size,
+ &map_type,
+ layer_index as f32,
+ ) * Transform::from_xyz(offset_x, -offset_y, 0.0),
+ map_type,
+ render_settings: *render_settings,
+ ..Default::default()
+ });
+
+ layer_storage
+ .storage
+ .insert(layer_index as u32, layer_entity);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/loading.rs b/src/loading.rs
index 0caa6f0..aa1e52a 100644
--- a/src/loading.rs
+++ b/src/loading.rs
@@ -4,7 +4,7 @@ use bevy_asset_loader::{
loading_state::{config::ConfigureLoadingState, LoadingState, LoadingStateAppExt},
};
-use crate::GameState;
+use crate::{loaders::tiled::TiledMap, GameState};
pub struct LoadingPlugin;
@@ -13,6 +13,7 @@ impl Plugin for LoadingPlugin {
app.add_loading_state(
LoadingState::new(GameState::Loading)
.continue_to_state(GameState::Menu)
+ .load_collection::()
.load_collection::(),
);
}
@@ -38,3 +39,9 @@ pub struct TextureAssets {
#[asset(path = "textures/forgejo.png")]
pub forgejo: Handle,
}
+
+#[derive(AssetCollection, Resource)]
+pub struct MapAssets {
+ #[asset(path = "maps/test.tmx")]
+ pub map: Handle,
+}
diff --git a/src/map.rs b/src/map.rs
new file mode 100644
index 0000000..826cb29
--- /dev/null
+++ b/src/map.rs
@@ -0,0 +1,18 @@
+use bevy::prelude::*;
+
+use crate::{loaders::tiled::TiledMapBundle, loading::MapAssets, GameState};
+
+pub struct MapPlugin;
+
+impl Plugin for MapPlugin {
+ fn build(&self, app: &mut App) {
+ app.add_systems(OnEnter(GameState::Playing), setup);
+ }
+}
+
+fn setup(mut commands: Commands, maps: Res) {
+ commands.spawn(TiledMapBundle {
+ tiled_map: maps.map.clone(),
+ ..default()
+ });
+}
diff --git a/src/player.rs b/src/player.rs
index 6194d12..68b843e 100644
--- a/src/player.rs
+++ b/src/player.rs
@@ -47,7 +47,7 @@ impl AnimationConfig {
fn spawn(mut commands: Commands, textures: Res) {
let mut camera = Camera2dBundle::default();
- camera.projection.scale *= 0.25;
+ camera.projection.scale *= 0.5;
commands.spawn(camera);
let animation_config = AnimationConfig::new(0, 3, 10);
@@ -55,6 +55,7 @@ fn spawn(mut commands: Commands, textures: Res) {
commands.spawn((
SpriteBundle {
texture: textures.witch.clone(),
+ transform: Transform::from_xyz(0.0, 0.0, 100.0),
..default()
},
TextureAtlas::from(textures.witch_layout.clone()),