diff --git a/assets/fonts/courneuf-family/Courneuf-Regular.ttf b/assets/fonts/courneuf-family/Courneuf-Regular.ttf new file mode 100644 index 0000000..08f7352 Binary files /dev/null and b/assets/fonts/courneuf-family/Courneuf-Regular.ttf differ diff --git a/assets/fonts/courneuf-family/README.md b/assets/fonts/courneuf-family/README.md new file mode 100644 index 0000000..a810542 --- /dev/null +++ b/assets/fonts/courneuf-family/README.md @@ -0,0 +1 @@ +https://fontlibrary.org/en/font/courneuf-family diff --git a/src/character.rs b/src/character.rs index b76f945..ef2072b 100644 --- a/src/character.rs +++ b/src/character.rs @@ -2,7 +2,8 @@ use bevy::{ app::{Plugin, Startup, Update}, asset::{AssetServer, Assets}, math::{UVec2, Vec2}, - prelude::{default, Commands, Component, Deref, DerefMut, Local, Query, Res, ResMut}, + prelude::*, + reflect::Reflect, sprite::{Sprite, SpriteBundle, TextureAtlas, TextureAtlasLayout}, time::{Time, Timer, TimerMode}, transform::components::Transform, @@ -47,11 +48,31 @@ pub struct Character { state: CharacterState, } +#[derive(Component, Reflect)] +pub struct CoinPouch(pub u64); + +#[derive(Component, Reflect)] +pub struct HealthPoints { + pub max_full_hearts: u8, + pub current: u8, +} + +impl HealthPoints { + fn full(hearts: u8) -> Self { + HealthPoints { + max_full_hearts: hearts, + current: hearts * 2, + } + } +} + pub struct CharacterPlugin; impl Plugin for CharacterPlugin { fn build(&self, app: &mut bevy::prelude::App) { - app.add_systems(Startup, startup) + app.register_type::() + .register_type::() + .add_systems(Startup, startup) .add_systems(Update, (movement, animate)); } } @@ -65,6 +86,7 @@ fn startup( let atlas_layout = TextureAtlasLayout::from_grid(UVec2::new(16, 16), 8, 5, None, None); let atlas_layout_handle = texture_atlases.add(atlas_layout); let texture = asset_server.load("bgp_catdev/player_and_ui/Basic_Player.png"); + commands.spawn(( Character { movement_speed: CHARACTER_MOVEMENT_SPEED as f32, @@ -113,6 +135,8 @@ fn startup( }, //LockedAxes::ROTATION_LOCKED, AnimationTimer(Timer::from_seconds(0.1, TimerMode::Repeating)), + CoinPouch(50), + HealthPoints::full(5), PIXEL_PERFECT_LAYERS, )); } diff --git a/src/game.rs b/src/game.rs index 83be21b..50710c8 100644 --- a/src/game.rs +++ b/src/game.rs @@ -2,6 +2,7 @@ use bevy::app::{PluginGroup, PluginGroupBuilder}; use crate::{ camera::CameraPlugin, character::CharacterPlugin, control::ControlPlugin, map::MapPlugin, + ui::UIPlugin, }; pub struct GamePlugins; @@ -12,6 +13,7 @@ impl PluginGroup for GamePlugins { .add(MapPlugin) .add(CameraPlugin) .add(ControlPlugin) + .add(UIPlugin) //.add(PhysicsPlugin) .add_after::(CharacterPlugin) } diff --git a/src/main.rs b/src/main.rs index 62e3516..2190a10 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,6 +20,7 @@ mod game; mod game_world; mod map; mod physics; +mod ui; mod utils; use bevy::asset::AssetMetaCheck; diff --git a/src/ui.rs b/src/ui.rs new file mode 100644 index 0000000..0b1b10e --- /dev/null +++ b/src/ui.rs @@ -0,0 +1,212 @@ +use bevy::{ + app::{Plugin, Startup}, + prelude::*, +}; + +use crate::{ + character::{Character, CoinPouch, HealthPoints}, + HIGH_RES_LAYERS, +}; + +pub struct UIPlugin; + +impl Plugin for UIPlugin { + fn build(&self, app: &mut bevy::prelude::App) { + app.add_systems(Startup, (load_assets, startup).chain()) + .add_systems(FixedUpdate, (update_coins, update_health_points)); + } +} + +#[derive(Component)] +struct CoinPouchNodeUI; + +#[derive(Component)] +struct CoinPouchTextUI; + +#[derive(Component)] +struct HealthPointsNodeUI; + +#[derive(Component)] +struct HealthPointIconUI; + +#[derive(Resource)] +struct TextFont(Handle); + +#[derive(Resource)] +struct HeartsAndCoinsTexture(Handle); + +#[derive(Resource)] +struct HeartsAndCoinsTextureAtlas(Handle); + +fn load_assets( + mut commands: Commands, + asset_server: Res, + mut texture_atlases: ResMut>, +) { + let font_handle: Handle = asset_server.load("fonts/courneuf-family/Courneuf-Regular.ttf"); + commands.insert_resource(TextFont(font_handle)); + + let texture_handle: Handle = + asset_server.load("bgp_catdev/player_and_ui/Basic_HeartsAndCoins.png"); + commands.insert_resource(HeartsAndCoinsTexture(texture_handle)); + + let texture_atlas = TextureAtlasLayout::from_grid(UVec2::splat(16), 5, 2, None, None); + let texture_atlas_handle: Handle = texture_atlases.add(texture_atlas); + commands.insert_resource(HeartsAndCoinsTextureAtlas(texture_atlas_handle)); +} + +fn startup( + mut commands: Commands, + text_font_handle: Res, + texture_handle: Res, + texture_atlas_handle: Res, +) { + commands + .spawn(( + Name::new("Main UI node"), + NodeBundle { + style: Style { + width: Val::Percent(100.0), + height: Val::Percent(100.0), + align_items: AlignItems::FlexStart, + justify_content: JustifyContent::FlexStart, + flex_direction: FlexDirection::Column, + padding: UiRect::axes(Val::Px(20.0), Val::Px(10.0)), + ..default() + }, + ..default() + }, + HIGH_RES_LAYERS, + )) + .with_children(|parent| { + parent.spawn(( + Name::new("Health points UI"), + HealthPointsNodeUI, + NodeBundle { + style: Style { + width: Val::Percent(100.0), + align_items: AlignItems::Center, + ..default() + }, + ..default() + }, + )); + + parent + .spawn(( + Name::new("Coins UI"), + CoinPouchNodeUI, + NodeBundle { + style: Style { + width: Val::Percent(100.0), + align_items: AlignItems::Center, + ..default() + }, + ..default() + }, + )) + .with_children(|parent| { + parent.spawn(( + ImageBundle { + style: Style { + width: Val::Px(64.), + height: Val::Px(64.), + ..default() + }, + image: UiImage::new(texture_handle.0.clone()), + ..default() + }, + TextureAtlas { + layout: texture_atlas_handle.0.clone(), + index: 5, + }, + )); + + parent.spawn(( + CoinPouchTextUI, + TextBundle::from_sections([TextSection::new( + "0", + TextStyle { + font_size: 42.0, + font: text_font_handle.0.clone(), + ..default() + }, + )]), + )); + }); + }); +} + +fn update_coins( + coin_pouch_query: Query<&CoinPouch, With>, + mut coin_pouch_style_query: Query<&mut Style, With>, + mut coin_pouch_text_query: Query<&mut Text, With>, +) { + let mut coin_pouch_style = coin_pouch_style_query.single_mut(); + let mut coin_pouch_text = coin_pouch_text_query.single_mut(); + + match coin_pouch_query.get_single() { + Ok(CoinPouch(amount)) => { + coin_pouch_text.sections[0].value = amount.to_string(); + } + Err(_) => { + coin_pouch_style.display = Display::None; + } + }; +} + +fn update_health_points( + mut commands: Commands, + health_points_query: Query<&HealthPoints, With>, + mut health_points_ui_query: Query<(Entity, &mut Style), With>, + texture_handle: Res, + texture_atlas_handle: Res, +) { + let (health_points_ui_entity, mut health_points_style) = health_points_ui_query.single_mut(); + + match health_points_query.get_single() { + Ok(health_points) => { + let mut children = vec![]; + + let full_hearts = health_points.current / 2; + let half_hearts = health_points.current % 2; + + for index in 0..health_points.max_full_hearts { + let texture_atlas_index = if index < full_hearts { + 0 + } else if index < full_hearts + half_hearts { + 1 + } else { + 2 + }; + + let heart_point_entity = commands + .spawn(( + HealthPointIconUI, + ImageBundle { + style: Style { + width: Val::Px(64.), + height: Val::Px(64.), + ..default() + }, + image: UiImage::new(texture_handle.0.clone()), + ..default() + }, + TextureAtlas { + layout: texture_atlas_handle.0.clone(), + index: texture_atlas_index, + }, + )) + .id(); + children.push(heart_point_entity); + } + + let mut health_points_ui_entity_commands = commands.entity(health_points_ui_entity); + health_points_ui_entity_commands.despawn_descendants(); + health_points_ui_entity_commands.push_children(&children); + } + Err(_) => { + health_points_style.display = Display::None; + } + }; +}