Merge pull request #21 from kristoferssolo/feat/UI

This commit is contained in:
Kristofers Solo 2025-01-05 19:06:37 +02:00 committed by GitHub
commit 285d35d87e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 69 additions and 144 deletions

View File

@ -309,7 +309,6 @@ pub enum Screen {
Splash, Splash,
Loading, Loading,
Title, Title,
Credits,
Gameplay, Gameplay,
Victory, Victory,
Leaderboard, Leaderboard,

View File

@ -2,3 +2,4 @@ pub const MOVEMENT_THRESHOLD: f32 = 0.01;
pub const WALL_OVERLAP_MODIFIER: f32 = 1.25; pub const WALL_OVERLAP_MODIFIER: f32 = 1.25;
pub const FLOOR_Y_OFFSET: u8 = 200; pub const FLOOR_Y_OFFSET: u8 = 200;
pub const MOVEMENT_COOLDOWN: f32 = 1.0; // one second cooldown pub const MOVEMENT_COOLDOWN: f32 = 1.0; // one second cooldown
pub const TITLE: &'static str = "Maze Ascension: The Labyrinth of Echoes";

View File

@ -1,7 +1,8 @@
use crate::{ use crate::{
floor::components::{CurrentFloor, Floor}, floor::components::{CurrentFloor, Floor},
maze::{components::MazeConfig, events::RespawnMaze, GlobalMazeConfig, MazePluginLoaded}, maze::{components::MazeConfig, events::RespawnMaze, GlobalMazeConfig},
player::events::RespawnPlayer, player::events::RespawnPlayer,
screens::Screen,
}; };
use bevy::{prelude::*, window::PrimaryWindow}; use bevy::{prelude::*, window::PrimaryWindow};
use bevy_egui::{ use bevy_egui::{
@ -13,9 +14,12 @@ use rand::{thread_rng, Rng};
use std::ops::RangeInclusive; use std::ops::RangeInclusive;
pub fn maze_controls_ui(world: &mut World) { pub fn maze_controls_ui(world: &mut World) {
if world.get_resource::<MazePluginLoaded>().is_none() { if let Some(state) = world.get_resource::<State<Screen>>() {
// Check if the current state is NOT Gameplay
if *state.get() != Screen::Gameplay {
return; return;
} }
}
let Ok(egui_context) = world let Ok(egui_context) = world
.query_filtered::<&mut EguiContext, With<PrimaryWindow>>() .query_filtered::<&mut EguiContext, With<PrimaryWindow>>()

View File

@ -2,7 +2,7 @@ mod despawn;
mod movement; mod movement;
mod spawn; mod spawn;
use crate::maze::MazePluginLoaded; use crate::screens::Screen;
use bevy::prelude::*; use bevy::prelude::*;
use despawn::despawn_floor; use despawn::despawn_floor;
use movement::{handle_floor_transition_events, move_floors}; use movement::{handle_floor_transition_events, move_floors};
@ -18,6 +18,6 @@ pub(super) fn plugin(app: &mut App) {
move_floors, move_floors,
) )
.chain() .chain()
.run_if(resource_exists::<MazePluginLoaded>), .run_if(in_state(Screen::Gameplay)),
); );
} }

View File

@ -33,7 +33,7 @@ pub(super) fn spawn_floor(
floor: target_floor, floor: target_floor,
config: MazeConfig { config: MazeConfig {
start_pos: config.end_pos, start_pos: config.end_pos,
radius: config.radius + 2, radius: config.radius + 1,
..default() ..default()
}, },
}); });

View File

@ -14,6 +14,7 @@ use bevy::{
audio::{AudioPlugin, Volume}, audio::{AudioPlugin, Volume},
prelude::*, prelude::*,
}; };
use constants::TITLE;
use theme::{palette::rose_pine, prelude::ColorScheme}; use theme::{palette::rose_pine, prelude::ColorScheme};
pub struct AppPlugin; pub struct AppPlugin;
@ -41,7 +42,7 @@ impl Plugin for AppPlugin {
}) })
.set(WindowPlugin { .set(WindowPlugin {
primary_window: Window { primary_window: Window {
title: "Maze Ascension: The Labyrinth of Echoes".to_string(), title: TITLE.to_string(),
canvas: Some("#bevy".to_string()), canvas: Some("#bevy".to_string()),
fit_canvas_to_parent: true, fit_canvas_to_parent: true,
prevent_default_event_handling: true, prevent_default_event_handling: true,
@ -103,9 +104,6 @@ fn spawn_camera(mut commands: Commands) {
} }
fn load_background(mut commands: Commands) { fn load_background(mut commands: Commands) {
#[cfg(feature = "dev")]
let colorcheme = rose_pine::RosePine::Base;
#[cfg(not(feature = "dev"))]
let colorcheme = rose_pine::RosePineDawn::Base; let colorcheme = rose_pine::RosePineDawn::Base;
commands.insert_resource(ClearColor(colorcheme.to_color())); commands.insert_resource(ClearColor(colorcheme.to_color()));
} }

View File

@ -79,7 +79,7 @@ impl MazeConfig {
impl Default for MazeConfig { impl Default for MazeConfig {
fn default() -> Self { fn default() -> Self {
Self::new( Self::new(
8, 4,
HexOrientation::Flat, HexOrientation::Flat,
None, None,
&GlobalMazeConfig::default(), &GlobalMazeConfig::default(),
@ -93,7 +93,6 @@ fn generate_pos<R: Rng>(radius: u16, rng: &mut R) -> Hex {
loop { loop {
let q = rng.gen_range(-radius..=radius); let q = rng.gen_range(-radius..=radius);
let r = rng.gen_range(-radius..=radius); let r = rng.gen_range(-radius..=radius);
let s = -q - r; // Calculate third coordinate (axial coordinates: q + r + s = 0) let s = -q - r; // Calculate third coordinate (axial coordinates: q + r + s = 0)
// Check if the position is within the hexagonal radius // Check if the position is within the hexagonal radius

View File

@ -9,7 +9,7 @@ pub mod triggers;
use bevy::{ecs::system::RunSystemOnce, prelude::*}; use bevy::{ecs::system::RunSystemOnce, prelude::*};
use components::HexMaze; use components::HexMaze;
use events::{DespawnMaze, RespawnMaze, SpawnMaze}; use events::{DespawnMaze, RespawnMaze, SpawnMaze};
pub use resources::{GlobalMazeConfig, MazePluginLoaded}; pub use resources::GlobalMazeConfig;
pub(super) fn plugin(app: &mut App) { pub(super) fn plugin(app: &mut App) {
app.init_resource::<GlobalMazeConfig>() app.init_resource::<GlobalMazeConfig>()
@ -22,5 +22,4 @@ pub(super) fn plugin(app: &mut App) {
pub fn spawn_level_command(world: &mut World) { pub fn spawn_level_command(world: &mut World) {
let _ = world.run_system_once(systems::setup::setup); let _ = world.run_system_once(systems::setup::setup);
world.insert_resource(MazePluginLoaded);
} }

View File

@ -1,9 +1,5 @@
use bevy::prelude::*; use bevy::prelude::*;
#[derive(Debug, Default, Reflect, Resource)]
#[reflect(Resource)]
pub struct MazePluginLoaded;
#[derive(Debug, Reflect, Resource, Clone)] #[derive(Debug, Reflect, Resource, Clone)]
#[reflect(Resource)] #[reflect(Resource)]
pub struct GlobalMazeConfig { pub struct GlobalMazeConfig {

View File

@ -8,6 +8,7 @@ use crate::{
events::SpawnMaze, events::SpawnMaze,
resources::GlobalMazeConfig, resources::GlobalMazeConfig,
}, },
screens::Screen,
theme::palette::rose_pine::RosePineDawn, theme::palette::rose_pine::RosePineDawn,
}; };
@ -53,6 +54,7 @@ pub(crate) fn spawn_maze(
config.clone(), config.clone(),
Transform::from_translation(Vec3::ZERO.with_y(y_offset)), Transform::from_translation(Vec3::ZERO.with_y(y_offset)),
Visibility::Visible, Visibility::Visible,
StateScoped(Screen::Gameplay),
)) ))
.insert_if(CurrentFloor, || *floor == 1) .insert_if(CurrentFloor, || *floor == 1)
.id(); .id();

View File

@ -222,6 +222,7 @@ impl From<LogicalDirection> for EdgeDirection {
direction.rotate_cw(0) direction.rotate_cw(0)
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@ -3,7 +3,7 @@ mod movement;
pub mod setup; pub mod setup;
mod vertical_transition; mod vertical_transition;
use crate::maze::MazePluginLoaded; use crate::screens::Screen;
use bevy::prelude::*; use bevy::prelude::*;
use input::player_input; use input::player_input;
use movement::player_movement; use movement::player_movement;
@ -17,6 +17,6 @@ pub(super) fn plugin(app: &mut App) {
player_movement.after(player_input), player_movement.after(player_input),
handle_floor_transition, handle_floor_transition,
) )
.run_if(resource_exists::<MazePluginLoaded>), .run_if(in_state(Screen::Gameplay)),
); );
} }

View File

@ -6,6 +6,7 @@ use crate::{
components::{CurrentPosition, Player}, components::{CurrentPosition, Player},
events::SpawnPlayer, events::SpawnPlayer,
}, },
screens::Screen,
}; };
use bevy::prelude::*; use bevy::prelude::*;
@ -34,5 +35,6 @@ pub(super) fn spawn_player(
Mesh3d(meshes.add(generate_pill_mesh(player_radius, player_height / 2.))), Mesh3d(meshes.add(generate_pill_mesh(player_radius, player_height / 2.))),
MeshMaterial3d(materials.add(blue_material())), MeshMaterial3d(materials.add(blue_material())),
Transform::from_xyz(start_pos.x, y_offset, start_pos.y), Transform::from_xyz(start_pos.x, y_offset, start_pos.y),
StateScoped(Screen::Gameplay),
)); ));
} }

View File

@ -1,71 +0,0 @@
//! A credits screen that can be accessed from the title screen.
use bevy::prelude::*;
use crate::{asset_tracking::LoadResource, audio::Music, screens::Screen, theme::prelude::*};
pub(super) fn plugin(app: &mut App) {
app.add_systems(OnEnter(Screen::Credits), spawn_credits_screen);
app.load_resource::<CreditsMusic>();
app.add_systems(OnEnter(Screen::Credits), play_credits_music);
app.add_systems(OnExit(Screen::Credits), stop_music);
}
fn spawn_credits_screen(mut commands: Commands) {
commands
.ui_root()
.insert(StateScoped(Screen::Credits))
.with_children(|children| {
children.header("Made by");
children.label("Joe Shmoe - Implemented aligator wrestling AI");
children.label("Jane Doe - Made the music for the alien invasion");
children.header("Assets");
children.label("Bevy logo - All rights reserved by the Bevy Foundation. Permission granted for splash screen use when unmodified.");
children.label("Ducky sprite - CC0 by Caz Creates Games");
children.label("Button SFX - CC0 by Jaszunio15");
children.label("Music - CC BY 3.0 by Kevin MacLeod");
children.button("Back").observe(enter_title_screen);
});
}
fn enter_title_screen(_trigger: Trigger<OnPress>, mut next_screen: ResMut<NextState<Screen>>) {
next_screen.set(Screen::Title);
}
#[derive(Resource, Asset, Reflect, Clone)]
pub struct CreditsMusic {
#[dependency]
music: Handle<AudioSource>,
entity: Option<Entity>,
}
impl FromWorld for CreditsMusic {
fn from_world(world: &mut World) -> Self {
let assets = world.resource::<AssetServer>();
Self {
music: assets.load("audio/music/Monkeys Spinning Monkeys.ogg"),
entity: None,
}
}
}
fn play_credits_music(mut commands: Commands, mut music: ResMut<CreditsMusic>) {
music.entity = Some(
commands
.spawn((
AudioPlayer::<AudioSource>(music.music.clone()),
PlaybackSettings::LOOP,
Music,
))
.id(),
);
}
fn stop_music(mut commands: Commands, mut music: ResMut<CreditsMusic>) {
if let Some(entity) = music.entity.take() {
commands.entity(entity).despawn_recursive();
}
}

View File

@ -4,7 +4,7 @@
use bevy::prelude::*; use bevy::prelude::*;
use crate::{ use crate::{
screens::{credits::CreditsMusic, gameplay::GameplayMusic, Screen}, screens::{gameplay::GameplayMusic, Screen},
theme::{interaction::InteractionAssets, prelude::*}, theme::{interaction::InteractionAssets, prelude::*},
}; };
@ -21,8 +21,8 @@ fn spawn_loading_screen(mut commands: Commands) {
commands commands
.ui_root() .ui_root()
.insert(StateScoped(Screen::Loading)) .insert(StateScoped(Screen::Loading))
.with_children(|children| { .with_children(|parent| {
children.label("Loading...").insert(Node { parent.label("Loading...").insert(Node {
justify_content: JustifyContent::Center, justify_content: JustifyContent::Center,
..default() ..default()
}); });
@ -35,8 +35,7 @@ fn continue_to_title_screen(mut next_screen: ResMut<NextState<Screen>>) {
const fn all_assets_loaded( const fn all_assets_loaded(
interaction_assets: Option<Res<InteractionAssets>>, interaction_assets: Option<Res<InteractionAssets>>,
credits_music: Option<Res<CreditsMusic>>,
gameplay_music: Option<Res<GameplayMusic>>, gameplay_music: Option<Res<GameplayMusic>>,
) -> bool { ) -> bool {
interaction_assets.is_some() && credits_music.is_some() && gameplay_music.is_some() interaction_assets.is_some() && gameplay_music.is_some()
} }

View File

@ -1,6 +1,5 @@
//! The game's main screen states and transitions between them. //! The game's main screen states and transitions between them.
mod credits;
mod gameplay; mod gameplay;
mod loading; mod loading;
mod splash; mod splash;
@ -13,7 +12,6 @@ pub(super) fn plugin(app: &mut App) {
app.enable_state_scoped_entities::<Screen>(); app.enable_state_scoped_entities::<Screen>();
app.add_plugins(( app.add_plugins((
credits::plugin,
gameplay::plugin, gameplay::plugin,
loading::plugin, loading::plugin,
splash::plugin, splash::plugin,
@ -29,6 +27,5 @@ pub enum Screen {
#[cfg_attr(feature = "dev", default)] #[cfg_attr(feature = "dev", default)]
Loading, Loading,
Title, Title,
Credits,
Gameplay, Gameplay,
} }

View File

@ -12,12 +12,19 @@ fn spawn_title_screen(mut commands: Commands) {
commands commands
.ui_root() .ui_root()
.insert(StateScoped(Screen::Title)) .insert(StateScoped(Screen::Title))
.with_children(|children| { .with_children(|parent| {
children.button("Play").observe(enter_gameplay_screen); parent
children.button("Credits").observe(enter_credits_screen); .spawn(Node {
bottom: Val::Px(70.),
..default()
})
.with_children(|parent| {
parent.header("Maze Ascension");
});
parent.button("Play").observe(enter_gameplay_screen);
#[cfg(not(target_family = "wasm"))] #[cfg(not(target_family = "wasm"))]
children.button("Exit").observe(exit_app); parent.button("Quit").observe(exit_app);
}); });
} }
@ -25,10 +32,6 @@ fn enter_gameplay_screen(_trigger: Trigger<OnPress>, mut next_screen: ResMut<Nex
next_screen.set(Screen::Gameplay); next_screen.set(Screen::Gameplay);
} }
fn enter_credits_screen(_trigger: Trigger<OnPress>, mut next_screen: ResMut<NextState<Screen>>) {
next_screen.set(Screen::Credits);
}
#[cfg(not(target_family = "wasm"))] #[cfg(not(target_family = "wasm"))]
fn exit_app(_trigger: Trigger<OnPress>, mut app_exit: EventWriter<AppExit>) { fn exit_app(_trigger: Trigger<OnPress>, mut app_exit: EventWriter<AppExit>) {
app_exit.send(AppExit::Success); app_exit.send(AppExit::Success);

View File

@ -2,15 +2,6 @@ pub mod rose_pine;
use bevy::prelude::*; use bevy::prelude::*;
pub const BUTTON_HOVERED_BACKGROUND: Color = Color::srgb(0.186, 0.328, 0.573);
pub const BUTTON_PRESSED_BACKGROUND: Color = Color::srgb(0.286, 0.478, 0.773);
pub const BUTTON_TEXT: Color = Color::srgb(0.925, 0.925, 0.925);
pub const LABEL_TEXT: Color = Color::srgb(0.867, 0.827, 0.412);
pub const HEADER_TEXT: Color = Color::srgb(0.867, 0.827, 0.412);
pub const NODE_BACKGROUND: Color = Color::srgb(0.286, 0.478, 0.773);
const MAX_COLOR_VALUE: f32 = 255.; const MAX_COLOR_VALUE: f32 = 255.;
pub(super) const fn rgb_u8(red: u8, green: u8, blue: u8) -> Color { pub(super) const fn rgb_u8(red: u8, green: u8, blue: u8) -> Color {

View File

@ -1,7 +1,9 @@
//! Helper traits for creating common widgets. //! Helper traits for creating common widgets.
use bevy::{ecs::system::EntityCommands, prelude::*, ui::Val::*}; use bevy::{ecs::system::EntityCommands, prelude::*, ui::Val::*};
use rose_pine::RosePineDawn;
use super::prelude::ColorScheme;
use crate::theme::{interaction::InteractionPalette, palette::*}; use crate::theme::{interaction::InteractionPalette, palette::*};
/// An extension trait for spawning UI widgets. /// An extension trait for spawning UI widgets.
@ -22,7 +24,7 @@ impl<T: SpawnUi> Widgets for T {
Name::new("Button"), Name::new("Button"),
Button, Button,
ImageNode { ImageNode {
color: NODE_BACKGROUND, color: RosePineDawn::Surface.to_color(),
..default() ..default()
}, },
Node { Node {
@ -30,23 +32,26 @@ impl<T: SpawnUi> Widgets for T {
height: Px(65.0), height: Px(65.0),
justify_content: JustifyContent::Center, justify_content: JustifyContent::Center,
align_items: AlignItems::Center, align_items: AlignItems::Center,
border: UiRect::all(Px(4.)),
..default() ..default()
}, },
BorderRadius::all(Px(8.)),
BorderColor(RosePineDawn::Text.to_color()),
InteractionPalette { InteractionPalette {
none: NODE_BACKGROUND, none: RosePineDawn::HighlightLow.to_color(),
hovered: BUTTON_HOVERED_BACKGROUND, hovered: RosePineDawn::HighlightMed.to_color(),
pressed: BUTTON_PRESSED_BACKGROUND, pressed: RosePineDawn::HighlightHigh.to_color(),
}, },
)); ));
entity.with_children(|children| { entity.with_children(|parent| {
children.spawn_ui(( parent.spawn_ui((
Name::new("Button Text"), Name::new("Button Text"),
Text(text.into()), Text(text.into()),
TextFont { TextFont {
font_size: 40.0, font_size: 40.0,
..default() ..default()
}, },
TextColor(BUTTON_TEXT), TextColor(RosePineDawn::Text.to_color()),
)); ));
}); });
@ -63,38 +68,38 @@ impl<T: SpawnUi> Widgets for T {
align_items: AlignItems::Center, align_items: AlignItems::Center,
..default() ..default()
}, },
BackgroundColor(NODE_BACKGROUND),
)); ));
entity.with_children(|children| { entity.with_children(|parent| {
children.spawn_ui(( parent.spawn_ui((
Name::new("Header Text"), Name::new("Header Text"),
Text(text.into()), Text(text.into()),
TextFont { TextFont {
font_size: 40.0, font_size: 60.0,
..default() ..default()
}, },
TextColor(HEADER_TEXT), TextLayout {
justify: JustifyText::Center,
..default()
},
TextColor(RosePineDawn::Text.to_color()),
)); ));
}); });
entity entity
} }
fn label(&mut self, _text: impl Into<String>) -> EntityCommands { fn label(&mut self, text: impl Into<String>) -> EntityCommands {
let entity = self.spawn_ui(( let entity = self.spawn_ui((
Name::new("Label"), Name::new("Label"),
Text::default(), Text(text.into()),
// TextBundle::from_section( TextFont {
// text, font_size: 24.0,
// TextStyle { ..default()
// font_size: 24.0, },
// color: LABEL_TEXT, TextColor(RosePineDawn::Text.to_color()),
// ..default() Node {
// }, width: Px(500.),
// ) ..default()
// .with_style(Style { },
// width: Px(500.0),
// ..default()
// }),
)); ));
entity entity
} }
@ -117,7 +122,7 @@ impl Containers for Commands<'_, '_> {
justify_content: JustifyContent::Center, justify_content: JustifyContent::Center,
align_items: AlignItems::Center, align_items: AlignItems::Center,
flex_direction: FlexDirection::Column, flex_direction: FlexDirection::Column,
row_gap: Px(10.0), row_gap: Px(32.0),
position_type: PositionType::Absolute, position_type: PositionType::Absolute,
..default() ..default()
}, },