diff --git a/game_core/src/assets/loader.rs b/game_core/src/assets/loader.rs new file mode 100644 index 0000000000000000000000000000000000000000..4d26426c8c46efc3e52eb6211f3fe49c9f61d5fc --- /dev/null +++ b/game_core/src/assets/loader.rs @@ -0,0 +1,102 @@ +use std::marker::PhantomData; + +use bevy::asset::LoadState; +use bevy::ecs::system::SystemParam; +use bevy::prelude::*; +use bevy::reflect::{TypePath, TypeUuid}; +use micro_banimate::definitions::AnimationSet; +use micro_ldtk::LdtkProject; +use micro_musicbox::prelude::AudioSource; + +use crate::assets::{AssetHandles, FixedAssetNameMapping, SpriteSheetConfig}; + +#[derive(SystemParam)] +pub struct AssetTypeLoader<'w, 's> { + pub handles: ResMut<'w, AssetHandles>, + pub asset_server: Res<'w, AssetServer>, + pub atlas: ResMut<'w, Assets<TextureAtlas>>, + #[system_param(ignore)] + marker: PhantomData<&'s usize>, +} + +macro_rules! load_basic_type { + ($name: tt, $type: ty => $key: ident) => { + pub fn $name(&mut self, assets: &[FixedAssetNameMapping]) -> Vec<Handle<$type>> { + self.load_list(assets, |loader, path, key| { + let handle: Handle<$type> = loader.asset_server.load(&path); + loader.handles.$key.insert(key, handle.clone()); + handle + }) + } + }; +} + +macro_rules! load_state { + ($container: expr => $key: ident) => { + $container + .asset_server + .get_group_load_state($container.handles.$key.values().map(|f| f.id)) + }; +} + +impl<'w, 's> AssetTypeLoader<'w, 's> { + fn load_list< + T: Sync + Send + TypeUuid + TypePath + 'static, + Loader: Fn(&mut AssetTypeLoader, String, String) -> Handle<T>, + >( + &mut self, + files: &[FixedAssetNameMapping], + load: Loader, + ) -> Vec<Handle<T>> { + files + .iter() + .map(|(path, key)| load(self, path.to_string(), key.to_string())) + .collect() + } + + load_basic_type!(load_images, Image => images); + load_basic_type!(load_audio, AudioSource => sounds); + load_basic_type!(load_font, Font => fonts); + load_basic_type!(load_animation, AnimationSet => animations); + load_basic_type!(load_ldtk, LdtkProject => ldtk_projects); + + pub fn load_spritesheet( + &mut self, + config: &SpriteSheetConfig, + assets: &[FixedAssetNameMapping], + ) -> Vec<Handle<Image>> { + self.load_list(assets, |loader, path, key| { + let handle: Handle<Image> = loader.asset_server.load(&path); + + loader + .handles + .images + .insert(key.to_string(), handle.clone()); + + let atlas = TextureAtlas::from_grid( + handle.clone(), + Vec2::new(config.tile_width as f32, config.tile_height as f32), + config.columns, + config.rows, + None, + None, + ); + + let atlas_handle = loader.atlas.add(atlas); + loader.handles.atlas.insert(key, atlas_handle); + + handle + }) + } + + pub fn get_all_load_state(&self) -> Vec<LoadState> { + let image_state = self + .asset_server + .get_group_load_state(self.handles.images.values().map(|f| f.id())); + let atlas_state = self + .asset_server + .get_group_load_state(self.handles.images.values().map(|f| f.id())); + + vec![image_state, atlas_state] + } +} diff --git a/game_core/src/assets/mod.rs b/game_core/src/assets/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..bfdb0730bbe18586b728b8904b8ba703de37205d --- /dev/null +++ b/game_core/src/assets/mod.rs @@ -0,0 +1,28 @@ +mod loader; +mod resources; +mod startup; + +mod _plugin { + use crate::assets::startup::{ + check_load_resources, start_load_resources, start_preload_resources, + }; + use crate::system::{run_in_setup, AppState}; + use bevy::prelude::*; + + pub struct AssetLoadingPlugin; + impl Plugin for AssetLoadingPlugin { + fn build(&self, app: &mut App) { + app.init_resource::<super::resources::AssetHandles>() + .add_systems( + OnEnter(AppState::Preload), + (start_preload_resources, start_load_resources), + ) + .add_systems(Update, check_load_resources.run_if(run_in_setup)); + } + } +} + +pub use _plugin::AssetLoadingPlugin; +pub(self) use loader::AssetTypeLoader; +pub use resources::AssetHandles; +pub(self) use resources::{AssetNameMapping, FixedAssetNameMapping, SpriteSheetConfig}; diff --git a/game_core/src/assets/resources.rs b/game_core/src/assets/resources.rs new file mode 100644 index 0000000000000000000000000000000000000000..b2cb80874fe022caabd0a9a6558e49810fd47fab --- /dev/null +++ b/game_core/src/assets/resources.rs @@ -0,0 +1,92 @@ +use bevy::prelude::*; +use bevy::utils::HashMap; +use micro_banimate::definitions::AnimationSet; +use micro_ldtk::LdtkProject; +use micro_musicbox::prelude::AudioSource; +use micro_musicbox::utilities::{SuppliesAudio, TrackType}; + +#[derive(Copy, Clone, Debug)] +pub struct SpriteSheetConfig { + pub tile_width: usize, + pub tile_height: usize, + pub columns: usize, + pub rows: usize, +} + +impl SpriteSheetConfig { + pub fn squares(tile_wh: usize, columns: usize, rows: usize) -> Self { + Self { + tile_width: tile_wh, + tile_height: tile_wh, + columns, + rows, + } + } + + pub fn rectangles(tile_width: usize, tile_height: usize, columns: usize, rows: usize) -> Self { + Self { + tile_width, + tile_height, + columns, + rows, + } + } +} + +#[derive(Default, Resource)] +pub struct AssetHandles { + pub images: HashMap<String, Handle<Image>>, + pub atlas: HashMap<String, Handle<TextureAtlas>>, + pub sounds: HashMap<String, Handle<AudioSource>>, + pub fonts: HashMap<String, Handle<Font>>, + pub animations: HashMap<String, Handle<AnimationSet>>, + pub ldtk_projects: HashMap<String, Handle<LdtkProject>>, +} + +macro_rules! fetch_wrapper { + ($name: tt, $type: ty => $key: ident) => { + pub fn $name<T: ToString>(&self, name: T) -> Handle<$type> { + let key = name.to_string(); + match self.$key.get(&key) { + Some(handle) => handle.clone_weak(), + None => { + let keys = self.$key.keys(); + panic!( + "\n\nTried to fetch {} asset with a missing key: {}.\nPossible keys: {}\n\n", + stringify!($name), + name.to_string(), + keys.map(|k| format!("'{}'", k)) + .collect::<Vec<String>>() + .join(", ") + ) + } + } + } + }; +} + +impl AssetHandles { + fetch_wrapper!(image, Image => images); + fetch_wrapper!(atlas, TextureAtlas => atlas); + fetch_wrapper!(sound, AudioSource => sounds); + fetch_wrapper!(font, Font => fonts); + fetch_wrapper!(animation, AnimationSet => animations); + fetch_wrapper!(ldtk, LdtkProject => ldtk_projects); +} + +impl SuppliesAudio for AssetHandles { + fn resolve_track_name<T: ToString>(&self, name: T) -> TrackType<String> { + if self.sounds.contains_key(&name.to_string()) { + TrackType::Single(name.to_string()) + } else { + TrackType::Missing + } + } + + fn get_audio_track<T: ToString>(&self, name: T) -> Option<Handle<AudioSource>> { + self.sounds.get(&name.to_string()).map(Handle::clone_weak) + } +} + +pub type AssetNameMapping = (String, String); +pub type FixedAssetNameMapping = (&'static str, &'static str); diff --git a/game_core/src/assets/startup.rs b/game_core/src/assets/startup.rs new file mode 100644 index 0000000000000000000000000000000000000000..ef4c6df9ae0bbb63b1d56e0a080aa287a3df5956 --- /dev/null +++ b/game_core/src/assets/startup.rs @@ -0,0 +1,112 @@ +use bevy::asset::LoadState; +use bevy::prelude::*; + +use crate::assets::{AssetTypeLoader, SpriteSheetConfig}; +use crate::system::AppState; + +pub fn start_preload_resources( + mut _commands: Commands, + mut next_state: ResMut<NextState<AppState>>, +) { + // TODO: Add preload commands here + next_state.set(AppState::Setup); +} + +pub fn start_load_resources(mut loader: AssetTypeLoader) { + loader.load_images(&[ + ("splash.png", "splash"), + ("cresthollow.png", "menu_bg"), + ("interface/basic_frame.png", "basic_frame"), + ("interface/banner.png", "banner"), + ("interface/button_pressed.png", "button_down"), + ("interface/button_raised.png", "button_up"), + ("interface/button_hover.png", "button_over"), + ("interface/vertical_bar.png", "resource_bar"), + ("interface/vertical_bar_bg.png", "resource_bar_bg"), + ("interface/vertical_bar_fg.png", "resource_bar_fg"), + ("interface/health_bar.png", "bar_red"), + ("interface/mana_bar.png", "bar_blue"), + ("interface/stamina_bar.png", "bar_yellow"), + ("interface/energy_bar.png", "bar_green"), + ]); + + loader.load_audio(&[ + ("splash_sting.ogg", "splash_sting"), + ("music/menu.ogg", "menu_track"), + ("effects/running_001.ogg", "running_001"), + ("effects/fire_crackle_001.ogg", "fire_crackle_001"), + ("effects/cave_ambience.ogg", "cave_ambience"), + ("effects/ui_confirm.ogg", "ui_confirm"), + ("effects/ui_ping.ogg", "ui_ping"), + ]); + + loader.load_font(&[ + ("fonts/royal.ttf", "royal"), + ("fonts/equipe.ttf", "equipe"), + ("fonts/cursed.ttf", "cursed"), + ("fonts/cursed_bold.ttf", "cursed_bold"), + ]); + + loader.load_spritesheet( + &SpriteSheetConfig::squares(16, 32, 64), + &[ + ("sheets/items.png", "items"), + ("sheets/fx.png", "fx"), + ("sheets/or_interface.png", "interface"), + ("sheets/projectiles.png", "projectiles"), + ], + ); + + loader.load_spritesheet( + &SpriteSheetConfig::squares(32, 16, 32), + &[ + ("sheets/creatures/town_guard_human.png", "town_guard_human"), + ("sheets/creatures/town_guard_orc.png", "town_guard_orc"), + ("sheets/creatures/town_guard_dwarf.png", "town_guard_dwarf"), + ("sheets/creatures/town_guard_elf.png", "town_guard_elf"), + ], + ); + + loader.load_spritesheet( + &SpriteSheetConfig::squares(32, 32, 32), + &[ + ("sheets/creatures/human_base.png", "human_base"), + ("sheets/creatures/goblin_base.png", "goblin_base"), + ("sheets/creatures/skeleton.png", "skeleton"), + ("sheets/creatures/flame_acolyte.png", "flame_acolyte"), + ("sheets/creatures/divine_warrior.png", "divine_warrior"), + ], + ); + + loader.load_spritesheet( + &SpriteSheetConfig::squares(16, 64, 64), + &[ + ("sheets/world_dawn.png", "world"), + ("sheets/characters.png", "characters"), + ], + ); + + loader.load_animation(&[ + ("animations/humanoid.anim.json", "humanoid"), + ("animations/skeleton.anim.json", "skeleton"), + ("animations/flame_acolyte.anim.json", "flame_acolyte"), + ("animations/guards.anim.json", "town_guards"), + ]); + + loader.load_ldtk(&[ + ("ldtk/dawnish.ldtk", "tile_data"), + ("ldtk/cresthollow_world.ldtk", "cresthollow_world"), + ]); +} + +pub fn check_load_resources( + mut commands: Commands, + loader: AssetTypeLoader, + mut next_state: ResMut<NextState<AppState>>, +) { + let load_states = loader.get_all_load_state(); + if load_states.iter().all(|state| *state == LoadState::Loaded) { + info!("Assets loaded successfully"); + next_state.set(AppState::Splash); + } +}