Merge pull request #11 from kristoferssolo/feature/floors

This commit is contained in:
Kristofers Solo 2025-01-01 22:55:02 +02:00 committed by GitHub
commit ead980b7fe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
40 changed files with 780 additions and 133 deletions

View File

@ -27,9 +27,7 @@ jobs:
sweep-cache: true
- name: Run tests
run: |
cargo test --locked --workspace --all-features --all-targets
# Workaround for https://github.com/rust-lang/cargo/issues/6669
cargo test --locked --workspace --all-features --doc
cargo test --locked --workspace --no-default-features
# Run clippy lints.
clippy:
name: Clippy

210
Cargo.lock generated
View File

@ -2299,6 +2299,21 @@ dependencies = [
"libc",
]
[[package]]
name = "futures"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.31"
@ -2306,6 +2321,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
dependencies = [
"futures-core",
"futures-sink",
]
[[package]]
@ -2314,6 +2330,17 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
[[package]]
name = "futures-executor"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
dependencies = [
"futures-core",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-io"
version = "0.3.31"
@ -2333,6 +2360,53 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "futures-macro"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "futures-sink"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
[[package]]
name = "futures-task"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
[[package]]
name = "futures-timer"
version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24"
[[package]]
name = "futures-util"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-macro",
"futures-sink",
"futures-task",
"memchr",
"pin-project-lite",
"pin-utils",
"slab",
]
[[package]]
name = "fuzzy-matcher"
version = "0.3.7"
@ -2571,6 +2645,12 @@ dependencies = [
"foldhash",
]
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hermit-abi"
version = "0.4.0"
@ -2595,11 +2675,14 @@ checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df"
[[package]]
name = "hexlab"
version = "0.3.0"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b912e78d292803bc279aec3a4e2a0cdd0e0ac1540bcdc5d0f32cbfe9e4d234dc"
checksum = "7bd7c21f4e2c11d40473d1ae673905f4deae3b12104fa6d70eeef9ef385aceb6"
dependencies = [
"bevy",
"bevy_reflect",
"bevy_utils",
"glam",
"hexx",
"rand",
"thiserror 2.0.6",
@ -3083,7 +3166,7 @@ dependencies = [
[[package]]
name = "maze-ascension"
version = "0.2.2"
version = "0.3.0"
dependencies = [
"anyhow",
"bevy",
@ -3093,6 +3176,10 @@ dependencies = [
"hexx",
"log",
"rand",
"rstest",
"rstest_reuse",
"strum",
"test-log",
"thiserror 2.0.6",
"tracing",
]
@ -3749,6 +3836,12 @@ version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff"
[[package]]
name = "pin-utils"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "piper"
version = "0.2.4"
@ -4032,6 +4125,12 @@ version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "relative-path"
version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2"
[[package]]
name = "renderdoc-sys"
version = "1.1.0"
@ -4067,12 +4166,62 @@ version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
[[package]]
name = "rstest"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a2c585be59b6b5dd66a9d2084aa1d8bd52fbdb806eafdeffb52791147862035"
dependencies = [
"futures",
"futures-timer",
"rstest_macros",
"rustc_version",
]
[[package]]
name = "rstest_macros"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "825ea780781b15345a146be27eaefb05085e337e869bff01b4306a4fd4a9ad5a"
dependencies = [
"cfg-if",
"glob",
"proc-macro-crate",
"proc-macro2",
"quote",
"regex",
"relative-path",
"rustc_version",
"syn",
"unicode-ident",
]
[[package]]
name = "rstest_reuse"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3a8fb4672e840a587a66fc577a5491375df51ddb88f2a2c2a792598c326fe14"
dependencies = [
"quote",
"rand",
"syn",
]
[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustc_version"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
dependencies = [
"semver",
]
[[package]]
name = "rustix"
version = "0.38.42"
@ -4086,6 +4235,12 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "rustversion"
version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4"
[[package]]
name = "rustybuzz"
version = "0.14.1"
@ -4158,6 +4313,12 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2fdfc24bc566f839a2da4c4295b82db7d25a24253867d5c64355abb5799bdbe"
[[package]]
name = "semver"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba"
[[package]]
name = "send_wrapper"
version = "0.6.0"
@ -4318,6 +4479,28 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
[[package]]
name = "strum"
version = "0.26.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
dependencies = [
"strum_macros",
]
[[package]]
name = "strum_macros"
version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
dependencies = [
"heck",
"proc-macro2",
"quote",
"rustversion",
"syn",
]
[[package]]
name = "svg_fmt"
version = "0.4.4"
@ -4401,6 +4584,27 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "test-log"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dffced63c2b5c7be278154d76b479f9f9920ed34e7574201407f0b14e2bbb93"
dependencies = [
"test-log-macros",
"tracing-subscriber",
]
[[package]]
name = "test-log-macros"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5999e24eaa32083191ba4e425deb75cdf25efefabe5aaccb7446dd0d4122a3f5"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "thiserror"
version = "1.0.69"

View File

@ -1,7 +1,7 @@
[package]
name = "maze-ascension"
authors = ["Kristofers Solo <dev@kristofers.xyz>"]
version = "0.2.2"
version = "0.3.0"
edition = "2021"
[dependencies]
@ -18,12 +18,19 @@ tracing = { version = "0.1", features = [
"release_max_level_warn",
] }
hexx = { version = "0.19", features = ["bevy_reflect", "grid"] }
hexlab = { version = "0.3", features = ["bevy"] }
hexlab = { version = "0.5", features = ["bevy"] }
bevy-inspector-egui = { version = "0.28", optional = true }
bevy_egui = { version = "0.31", optional = true }
thiserror = "2.0"
anyhow = "1"
strum = { version = "0.26", features = ["derive"] }
[dev-dependencies]
rstest = "0.23"
rstest_reuse = "0.7"
test-log = { version = "0.2.16", default-features = false, features = [
"trace",
] }
[features]
default = [
@ -54,6 +61,8 @@ dev_native = [
too_many_arguments = "allow"
# Queries that access many components may trigger this lint.
type_complexity = "allow"
nursery = { level = "warn", priority = -1 }
unwrap_used = "warn"
# Compile with Performance Optimizations:

View File

@ -17,4 +17,3 @@ web-dev:
# Run web release
web-release:
trunk serve --release --no-default-features

View File

@ -1,8 +1,7 @@
//! A high-level way to load collections of asset handles as resources.
use std::collections::VecDeque;
use bevy::prelude::*;
use std::collections::VecDeque;
pub(super) fn plugin(app: &mut App) {
app.init_resource::<ResourceHandles>();
@ -51,7 +50,9 @@ fn load_resource_assets(world: &mut World) {
world.resource_scope(|world, mut resource_handles: Mut<ResourceHandles>| {
world.resource_scope(|world, assets: Mut<AssetServer>| {
for _ in 0..resource_handles.waiting.len() {
let (handle, insert_fn) = resource_handles.waiting.pop_front().unwrap();
let Some((handle, insert_fn)) = resource_handles.waiting.pop_front() else {
continue;
};
if assets.is_loaded_with_dependencies(&handle) {
insert_fn(world, &handle);
resource_handles.finished.push(handle);

3
src/constants.rs Normal file
View File

@ -0,0 +1,3 @@
pub const MOVEMENT_THRESHOLD: f32 = 0.01;
pub const WALL_OVERLAP_MODIFIER: f32 = 1.25;
pub const FLOOR_Y_OFFSET: u8 = 100;

View File

@ -12,7 +12,7 @@ use hexx::{Hex, HexOrientation};
use rand::{thread_rng, Rng};
use std::ops::RangeInclusive;
pub(crate) fn maze_controls_ui(world: &mut World) {
pub fn maze_controls_ui(world: &mut World) {
if world.get_resource::<MazePluginLoaded>().is_none() {
return;
}

View File

@ -1,3 +1,3 @@
mod maze_controls;
pub(crate) use maze_controls::maze_controls_ui;
pub use maze_controls::maze_controls_ui;

View File

@ -1,19 +1,59 @@
use bevy::prelude::*;
#[derive(Debug, Reflect, Component)]
#[derive(Debug, Reflect, Component, Deref, DerefMut)]
#[reflect(Component)]
pub struct Floor(pub u8);
#[derive(Debug, Reflect, Component)]
#[reflect(Component)]
pub struct TargetFloor(pub u8);
pub struct CurrentFloor;
#[derive(Debug, Reflect, Component)]
#[reflect(Component)]
pub struct CurrentFloor;
pub struct NextFloor;
#[derive(Debug, Reflect, Component, Deref, DerefMut)]
#[reflect(Component)]
pub struct FloorYTarget(pub f32);
impl Default for Floor {
fn default() -> Self {
Self(1)
}
}
impl Floor {
pub const fn increased(&self) -> Self {
Self(self.0.saturating_add(1))
}
pub fn decreased(&self) -> Self {
Self(self.0.saturating_sub(1).max(1))
}
}
#[cfg(test)]
mod tests {
use super::*;
use rstest::rstest;
#[rstest]
#[case(0, 1)]
#[case(1, 2)]
#[case(254, 255)]
#[case(255, 255)]
fn increase(#[case] input: u8, #[case] expected: u8) {
let floor = Floor(input);
assert_eq!(*floor.increased(), expected);
}
#[rstest]
#[case(0, 1)] // clamps to 1
#[case(1, 1)] // clamps to 1
#[case(2, 1)]
#[case(255, 254)]
fn decrease(#[case] input: u8, #[case] expected: u8) {
let floor = Floor(input);
assert_eq!(*floor.decreased(), expected);
}
}

View File

@ -1,20 +1,45 @@
use bevy::prelude::*;
use crate::maze::components::MazeConfig;
use super::components::Floor;
#[derive(Debug, Reflect, Event)]
pub struct SpawnFloor {
pub floor: u8,
pub config: MazeConfig,
#[derive(Debug, Clone, Copy, Reflect, Event, Default, PartialEq, Eq)]
pub enum TransitionFloor {
#[default]
Ascend,
Descend,
}
#[derive(Debug, Reflect, Event)]
pub struct RespawnFloor {
pub floor: u8,
pub config: MazeConfig,
impl TransitionFloor {
pub fn into_direction(&self) -> f32 {
self.into()
}
pub const fn opposite(&self) -> Self {
match self {
Self::Ascend => Self::Descend,
Self::Descend => Self::Ascend,
}
}
pub fn next_floor_num(&self, floor: &Floor) -> u8 {
match self {
Self::Ascend => *floor.increased(),
Self::Descend => *floor.decreased(),
}
}
}
#[derive(Debug, Reflect, Event)]
pub struct DespawnFloor {
pub floor: u8,
impl From<TransitionFloor> for f32 {
fn from(value: TransitionFloor) -> Self {
Self::from(&value)
}
}
impl From<&TransitionFloor> for f32 {
fn from(value: &TransitionFloor) -> Self {
match value {
TransitionFloor::Ascend => -1.,
TransitionFloor::Descend => 1.,
}
}
}

View File

@ -1,7 +1,14 @@
pub mod components;
pub mod events;
pub mod resources;
mod systems;
use bevy::prelude::*;
use events::TransitionFloor;
use resources::HighestFloor;
pub(super) fn plugin(_app: &mut App) {}
pub(super) fn plugin(app: &mut App) {
app.add_event::<TransitionFloor>()
.insert_resource(HighestFloor(1))
.add_plugins(systems::plugin);
}

5
src/floor/resources.rs Normal file
View File

@ -0,0 +1,5 @@
use bevy::prelude::*;
#[derive(Debug, Default, Reflect, Resource, PartialEq, Eq)]
#[reflect(Resource)]
pub struct HighestFloor(pub u8);

View File

View File

@ -0,0 +1,3 @@
use bevy::prelude::*;
pub const fn despawn_floor(mut _commands: Commands) {}

View File

@ -1 +1,23 @@
mod clear_events;
mod despawn;
mod movement;
mod spawn;
use crate::maze::MazePluginLoaded;
use bevy::prelude::*;
use despawn::despawn_floor;
use movement::{handle_floor_transition_events, move_floors};
use spawn::spawn_floor;
pub(super) fn plugin(app: &mut App) {
app.add_systems(
Update,
(
spawn_floor,
despawn_floor,
handle_floor_transition_events,
move_floors.after(handle_floor_transition_events),
)
.run_if(resource_exists::<MazePluginLoaded>),
);
}

View File

@ -0,0 +1,79 @@
use crate::{
constants::{FLOOR_Y_OFFSET, MOVEMENT_THRESHOLD},
floor::{
components::{CurrentFloor, FloorYTarget, NextFloor},
events::TransitionFloor,
},
maze::components::HexMaze,
player::components::{MovementSpeed, Player},
};
use bevy::prelude::*;
pub fn move_floors(
mut commands: Commands,
mut maze_query: Query<
(Entity, &mut Transform, &FloorYTarget),
(With<HexMaze>, With<FloorYTarget>),
>,
player_query: Query<&MovementSpeed, With<Player>>,
time: Res<Time>,
) {
let speed = player_query.get_single().map_or(100., |s| s.0);
let movement_distance = speed * time.delta_secs();
for (entity, mut transform, movement_state) in maze_query.iter_mut() {
let delta = movement_state.0 - transform.translation.y;
if delta.abs() > MOVEMENT_THRESHOLD {
let movement = delta.signum() * movement_distance.min(delta.abs());
transform.translation.y += movement;
} else {
transform.translation.y = movement_state.0;
commands.entity(entity).remove::<FloorYTarget>();
}
}
}
pub fn handle_floor_transition_events(
mut commands: Commands,
mut maze_query: Query<(Entity, &Transform, Option<&FloorYTarget>), With<HexMaze>>,
current_query: Query<Entity, With<CurrentFloor>>,
next_query: Query<Entity, With<NextFloor>>,
mut event_reader: EventReader<TransitionFloor>,
) {
let is_moving = maze_query
.iter()
.any(|(_, _, movement_state)| movement_state.is_some());
if is_moving {
return;
}
for event in event_reader.read() {
let direction = event.into();
let Some(current_entity) = current_query.get_single().ok() else {
continue;
};
let Some(next_entity) = next_query.get_single().ok() else {
continue;
};
for (entity, transforms, movement_state) in maze_query.iter_mut() {
let target_y = (FLOOR_Y_OFFSET as f32).mul_add(direction, transforms.translation.y);
if movement_state.is_none() {
commands.entity(entity).insert(FloorYTarget(target_y));
}
}
update_current_next_floor(&mut commands, current_entity, next_entity);
break;
}
}
fn update_current_next_floor(commands: &mut Commands, current_entity: Entity, next_entity: Entity) {
commands.entity(current_entity).remove::<CurrentFloor>();
commands
.entity(next_entity)
.remove::<NextFloor>()
.insert(CurrentFloor);
}

View File

@ -0,0 +1,35 @@
use crate::{
floor::{
components::{CurrentFloor, Floor, FloorYTarget},
events::TransitionFloor,
resources::HighestFloor,
},
maze::events::SpawnMaze,
};
use bevy::prelude::*;
pub(super) fn spawn_floor(
mut commands: Commands,
query: Query<&mut Floor, (With<CurrentFloor>, Without<FloorYTarget>)>,
mut event_reader: EventReader<TransitionFloor>,
mut highest_floor: ResMut<HighestFloor>,
) {
let Ok(floor) = query.get_single() else {
return;
};
for event in event_reader.read() {
let floor = event.next_floor_num(floor);
if floor == 1 && *event == TransitionFloor::Descend {
warn!("Cannot descend below floor 1");
return;
}
highest_floor.0 = highest_floor.0.max(floor);
info!("Creating level for floor {}", floor);
commands.trigger(SpawnMaze { floor, ..default() });
}
}

View File

@ -1,11 +1,12 @@
mod asset_tracking;
pub mod asset_tracking;
pub mod audio;
pub mod constants;
#[cfg(feature = "dev")]
mod dev_tools;
pub mod dev_tools;
pub mod floor;
pub mod maze;
pub mod player;
mod screens;
pub mod screens;
pub mod theme;
use bevy::{

View File

@ -1,25 +1,33 @@
use super::resources::GlobalMazeConfig;
use bevy::prelude::*;
use crate::{
constants::WALL_OVERLAP_MODIFIER,
theme::{palette::rose_pine::RosePine, prelude::ColorScheme},
};
use bevy::{prelude::*, utils::HashMap};
use std::f32::consts::FRAC_PI_2;
use strum::IntoEnumIterator;
const WALL_OVERLAP_MODIFIER: f32 = 1.25;
const HEX_SIDES: u32 = 6;
const WHITE_EMISSION_INTENSITY: f32 = 10.;
pub(crate) struct MazeAssets {
pub(crate) hex_mesh: Handle<Mesh>,
pub(crate) wall_mesh: Handle<Mesh>,
pub(crate) hex_material: Handle<StandardMaterial>,
pub(crate) wall_material: Handle<StandardMaterial>,
pub struct MazeAssets {
pub hex_mesh: Handle<Mesh>,
pub wall_mesh: Handle<Mesh>,
pub hex_material: Handle<StandardMaterial>,
pub wall_material: Handle<StandardMaterial>,
pub custom_materials: HashMap<RosePine, Handle<StandardMaterial>>,
}
impl MazeAssets {
pub(crate) fn new(
pub fn new(
meshes: &mut ResMut<Assets<Mesh>>,
materials: &mut ResMut<Assets<StandardMaterial>>,
global_config: &GlobalMazeConfig,
) -> MazeAssets {
MazeAssets {
) -> Self {
let custom_materials = RosePine::iter()
.map(|color| (color, materials.add(color.to_standart_material())))
.collect();
Self {
hex_mesh: meshes.add(generate_hex_mesh(
global_config.hex_size,
global_config.height,
@ -30,6 +38,7 @@ impl MazeAssets {
)),
hex_material: materials.add(white_material()),
wall_material: materials.add(Color::BLACK),
custom_materials,
}
}
}
@ -53,9 +62,8 @@ fn generate_square_mesh(depth: f32, wall_size: f32) -> Mesh {
Mesh::from(rectangular_prism).rotated_by(rotation)
}
fn white_material() -> StandardMaterial {
pub fn white_material() -> StandardMaterial {
StandardMaterial {
base_color: Color::WHITE,
emissive: LinearRgba::new(
WHITE_EMISSION_INTENSITY,
WHITE_EMISSION_INTENSITY,

View File

@ -1,15 +1,15 @@
use crate::floor::components::Floor;
use super::{errors::MazeConfigError, GlobalMazeConfig};
use super::GlobalMazeConfig;
use bevy::prelude::*;
use hexlab::HexMaze;
use hexlab::Maze;
use hexx::{Hex, HexLayout, HexOrientation};
use rand::{rngs::StdRng, thread_rng, Rng, SeedableRng};
#[derive(Debug, Reflect, Component)]
#[reflect(Component)]
#[require(MazeConfig, Floor)]
pub struct Maze(pub HexMaze);
#[require(MazeConfig, Floor, Maze)]
pub struct HexMaze;
#[derive(Debug, Reflect, Component)]
#[reflect(Component)]
@ -22,7 +22,7 @@ pub struct Wall;
#[derive(Debug, Reflect, Component, Clone)]
#[reflect(Component)]
pub struct MazeConfig {
pub radius: u32,
pub radius: u16,
pub start_pos: Hex,
pub end_pos: Hex,
pub seed: u64,
@ -31,19 +31,21 @@ pub struct MazeConfig {
impl MazeConfig {
fn new(
radius: u32,
radius: u16,
orientation: HexOrientation,
seed: Option<u64>,
global_conig: &GlobalMazeConfig,
) -> Result<Self, MazeConfigError> {
) -> Self {
let seed = seed.unwrap_or_else(|| thread_rng().gen());
let mut rng = StdRng::seed_from_u64(seed);
let start_pos = generate_pos(radius, &mut rng)?;
let end_pos = generate_pos(radius, &mut rng)?;
let start_pos = generate_pos(radius, &mut rng);
let end_pos = generate_pos(radius, &mut rng);
info!("Start pos: (q={}, r={})", start_pos.x, start_pos.y);
info!("End pos: (q={}, r={})", end_pos.x, end_pos.y);
info!(
"Start pos: (q={}, r={}). End pos: (q={}, r={})",
start_pos.x, start_pos.y, end_pos.x, end_pos.y
);
let layout = HexLayout {
orientation,
@ -51,23 +53,13 @@ impl MazeConfig {
..default()
};
Ok(Self {
Self {
radius,
start_pos,
end_pos,
seed,
layout,
})
}
pub fn new_unchecked(
radius: u32,
orientation: HexOrientation,
seed: Option<u64>,
global_conig: &GlobalMazeConfig,
) -> Self {
Self::new(radius, orientation, seed, global_conig)
.expect("Failed to create MazeConfig with supposedly safe values")
}
}
pub fn update(&mut self, global_conig: &GlobalMazeConfig) {
@ -77,14 +69,14 @@ impl MazeConfig {
impl Default for MazeConfig {
fn default() -> Self {
Self::new_unchecked(7, HexOrientation::Flat, None, &GlobalMazeConfig::default())
Self::new(8, HexOrientation::Flat, None, &GlobalMazeConfig::default())
}
}
fn generate_pos<R: Rng>(radius: u32, rng: &mut R) -> Result<Hex, MazeConfigError> {
let radius = i32::try_from(radius)?;
Ok(Hex::new(
fn generate_pos<R: Rng>(radius: u16, rng: &mut R) -> Hex {
let radius = radius as i32;
Hex::new(
rng.gen_range(-radius..radius),
rng.gen_range(-radius..radius),
))
)
}

View File

@ -14,7 +14,7 @@ pub enum MazeError {
#[error("Floor {0} not found")]
FloorNotFound(u8),
#[error("Failed to generate maze with config: {radius}, seed: {seed}")]
GenerationFailed { radius: u32, seed: u64 },
GenerationFailed { radius: u16, seed: u64 },
#[error("Invalid tile entity: {0:?}")]
TileNotFound(bevy::prelude::Entity),
#[error("Failed to create maze assets")]
@ -32,7 +32,7 @@ impl MazeError {
Self::ConfigurationError(msg.into())
}
pub fn generation_failed(radius: u32, seed: u64) -> Self {
pub const fn generation_failed(radius: u16, seed: u64) -> Self {
Self::GenerationFailed { radius, seed }
}
}

View File

@ -1,6 +1,5 @@
use bevy::prelude::*;
use super::components::MazeConfig;
use bevy::prelude::*;
#[derive(Debug, Reflect, Event)]
pub struct SpawnMaze {
@ -18,3 +17,12 @@ pub struct RespawnMaze {
pub struct DespawnMaze {
pub floor: u8,
}
impl Default for SpawnMaze {
fn default() -> Self {
Self {
floor: 1,
config: MazeConfig::default(),
}
}
}

View File

@ -7,7 +7,7 @@ mod systems;
mod triggers;
use bevy::{ecs::system::RunSystemOnce, prelude::*};
use components::Maze;
use components::HexMaze;
use events::{DespawnMaze, RespawnMaze, SpawnMaze};
pub use resources::{GlobalMazeConfig, MazePluginLoaded};
@ -16,7 +16,7 @@ pub(super) fn plugin(app: &mut App) {
.add_event::<SpawnMaze>()
.add_event::<RespawnMaze>()
.add_event::<DespawnMaze>()
.register_type::<Maze>()
.register_type::<HexMaze>()
.add_plugins((systems::plugin, triggers::plugin));
}

View File

@ -1,7 +1,6 @@
use crate::maze::{components::MazeConfig, events::SpawnMaze};
use crate::maze::events::SpawnMaze;
use bevy::prelude::*;
pub(crate) fn setup(mut commands: Commands) {
let config = MazeConfig::default();
commands.trigger(SpawnMaze { floor: 1, config });
pub fn setup(mut commands: Commands) {
commands.trigger(SpawnMaze::default());
}

View File

@ -2,9 +2,9 @@ use crate::maze::{
components::MazeConfig,
errors::{MazeError, MazeResult},
};
use hexlab::{GeneratorType, HexMaze, MazeBuilder};
use hexlab::prelude::*;
pub(crate) fn generate_maze(config: &MazeConfig) -> MazeResult<HexMaze> {
pub fn generate_maze(config: &MazeConfig) -> MazeResult<Maze> {
MazeBuilder::new()
.with_radius(config.radius)
.with_seed(config.seed)

View File

@ -1,7 +1,7 @@
pub mod common;
mod despawn;
mod respawn;
mod spawn;
pub mod spawn;
use bevy::prelude::*;
use despawn::despawn_maze;

View File

@ -1,12 +1,10 @@
use super::{common::generate_maze, spawn::spawn_maze_tiles};
use crate::{
floor::components::Floor,
maze::{
assets::MazeAssets, components::Maze, errors::MazeError, events::RespawnMaze,
GlobalMazeConfig,
},
maze::{assets::MazeAssets, errors::MazeError, events::RespawnMaze, GlobalMazeConfig},
};
use bevy::prelude::*;
use hexlab::Maze;
pub(super) fn respawn_maze(
trigger: Trigger<RespawnMaze>,
@ -30,7 +28,7 @@ pub(super) fn respawn_maze(
}
};
maze.0 = match generate_maze(config) {
*maze = match generate_maze(config) {
Ok(generated_maze) => generated_maze,
Err(e) => {
warn!("Failed to update floor ({floor}). {e}");
@ -43,7 +41,7 @@ pub(super) fn respawn_maze(
spawn_maze_tiles(
&mut commands,
entity,
&maze.0,
&maze,
&assets,
config,
&global_config,

View File

@ -1,19 +1,20 @@
use super::common::generate_maze;
use crate::{
floor::components::{CurrentFloor, Floor},
constants::FLOOR_Y_OFFSET,
floor::components::{CurrentFloor, Floor, NextFloor},
maze::{
assets::MazeAssets,
components::{Maze, MazeConfig, Tile, Wall},
components::{HexMaze, MazeConfig, Tile, Wall},
events::SpawnMaze,
resources::GlobalMazeConfig,
},
theme::palette::rose_pine::RosePine,
};
use bevy::prelude::*;
use hexlab::prelude::*;
use hexlab::prelude::{Tile as HexTile, *};
use hexx::HexOrientation;
use std::f32::consts::{FRAC_PI_2, FRAC_PI_3, FRAC_PI_6};
use super::common::generate_maze;
pub(super) fn spawn_maze(
trigger: Trigger<SpawnMaze>,
mut commands: Commands,
@ -23,23 +24,39 @@ pub(super) fn spawn_maze(
global_config: Res<GlobalMazeConfig>,
) {
let SpawnMaze { floor, config } = trigger.event();
if maze_query.iter().any(|(_, f, _)| f.0 == *floor) {
warn!("Floor {} already exists, skipping creation", floor);
return;
}
let maze = generate_maze(config).expect("Failed to generate maze during spawn");
let maze = match generate_maze(config) {
Ok(m) => m,
Err(e) => {
error!("Failed to generate maze for floor {floor}: {:?}", e);
return;
}
};
let y_offset = match *floor {
1 => 0,
_ => FLOOR_Y_OFFSET,
} as f32;
// (floor - 1) * FLOOR_Y_OFFSET
let entity = commands
.spawn((
Name::new(format!("Floor {}", floor)),
Maze(maze.clone()),
HexMaze,
maze.clone(),
Floor(*floor),
CurrentFloor, // TODO: remove
config.clone(),
Transform::from_translation(Vec3::ZERO),
Transform::from_translation(Vec3::ZERO.with_y(y_offset)),
Visibility::Visible,
))
.insert_if(CurrentFloor, || *floor == 1)
.insert_if(NextFloor, || *floor != 1)
.id();
let assets = MazeAssets::new(&mut meshes, &mut materials, &global_config);
@ -53,10 +70,10 @@ pub(super) fn spawn_maze(
);
}
pub(super) fn spawn_maze_tiles(
pub fn spawn_maze_tiles(
commands: &mut Commands,
parent_entity: Entity,
maze: &HexMaze,
maze: &Maze,
assets: &MazeAssets,
maze_config: &MazeConfig,
global_config: &GlobalMazeConfig,
@ -81,12 +98,26 @@ pub(super) fn spawn_single_hex_tile(
HexOrientation::Flat => Quat::from_rotation_y(FRAC_PI_6), // 30 degrees rotation
};
let material = match tile.pos() {
pos if pos == maze_config.start_pos => assets
.custom_materials
.get(&RosePine::Pine)
.cloned()
.unwrap_or_default(),
pos if pos == maze_config.end_pos => assets
.custom_materials
.get(&RosePine::Love)
.cloned()
.unwrap_or_default(),
_ => assets.hex_material.clone(),
};
parent
.spawn((
Name::new(format!("Hex {}", tile)),
Tile,
Mesh3d(assets.hex_mesh.clone()),
MeshMaterial3d(assets.hex_material.clone()),
MeshMaterial3d(material),
Transform::from_translation(world_pos).with_rotation(rotation),
))
.with_children(|parent| spawn_walls(parent, assets, tile.walls(), global_config));

View File

@ -1,6 +1,7 @@
use crate::theme::palette::rose_pine::PINE;
use bevy::prelude::*;
use crate::theme::{palette::rose_pine::RosePine, prelude::ColorScheme};
pub(super) fn generate_pill_mesh(radius: f32, half_length: f32) -> Mesh {
Mesh::from(Capsule3d {
radius,
@ -9,9 +10,10 @@ pub(super) fn generate_pill_mesh(radius: f32, half_length: f32) -> Mesh {
}
pub(super) fn blue_material() -> StandardMaterial {
let color = RosePine::Pine;
StandardMaterial {
base_color: PINE,
emissive: PINE.to_linear() * 3.,
base_color: color.to_color(),
emissive: color.to_linear_rgba() * 3.,
..default()
}
}

View File

@ -1,9 +1,10 @@
use crate::{
floor::components::CurrentFloor,
maze::components::{Maze, MazeConfig},
maze::components::MazeConfig,
player::components::{CurrentPosition, MovementTarget, Player},
};
use bevy::prelude::*;
use hexlab::prelude::*;
use hexx::{EdgeDirection, HexOrientation};
pub(super) fn player_input(
@ -24,7 +25,7 @@ pub(super) fn player_input(
continue;
};
let Some(tile) = maze.0.get_tile(current_pos) else {
let Some(tile) = maze.get(current_pos) else {
continue;
};

View File

@ -1,18 +1,22 @@
mod input;
mod movement;
pub mod setup;
mod vertical_transition;
use crate::maze::MazePluginLoaded;
use bevy::prelude::*;
use input::player_input;
use movement::player_movement;
use crate::maze::MazePluginLoaded;
use vertical_transition::handle_floor_transition;
pub(super) fn plugin(app: &mut App) {
app.add_systems(
Update,
(player_input, player_movement)
.chain()
(
player_input,
player_movement.after(player_input),
handle_floor_transition,
)
.run_if(resource_exists::<MazePluginLoaded>),
);
}

View File

@ -1,4 +1,5 @@
use crate::{
constants::MOVEMENT_THRESHOLD,
floor::components::CurrentFloor,
maze::components::MazeConfig,
player::components::{CurrentPosition, MovementSpeed, MovementTarget, Player},
@ -48,7 +49,7 @@ pub(super) fn player_movement(
}
fn should_complete_movement(current_pos: Vec3, target_pos: Vec3) -> bool {
(target_pos - current_pos).length() < 0.1
(target_pos - current_pos).length() < MOVEMENT_THRESHOLD
}
fn update_position(

View File

@ -1,6 +1,6 @@
use crate::player::events::SpawnPlayer;
use bevy::prelude::*;
pub(crate) fn setup(mut commands: Commands) {
pub fn setup(mut commands: Commands) {
commands.trigger(SpawnPlayer);
}

View File

@ -0,0 +1,37 @@
use bevy::prelude::*;
use crate::{
floor::{
components::{CurrentFloor, Floor},
events::TransitionFloor,
},
maze::components::MazeConfig,
player::components::{CurrentPosition, Player},
};
pub fn handle_floor_transition(
player_query: Query<&CurrentPosition, With<Player>>,
maze_query: Query<(&MazeConfig, &Floor), With<CurrentFloor>>,
mut event_writer: EventWriter<TransitionFloor>,
) {
let Ok((config, floor)) = maze_query.get_single() else {
warn!("Failed to get maze configuration for current floor - cannot ascend/descend player.");
return;
};
for current_hex in player_query.iter() {
// Check for ascending
if current_hex.0 == config.end_pos {
dbg!("Ascending");
event_writer.send(TransitionFloor::Ascend);
return;
}
// Check for descending
if current_hex.0 == config.start_pos && floor.0 != 1 {
dbg!("Descending");
event_writer.send(TransitionFloor::Descend);
return;
}
}
}

View File

@ -33,7 +33,7 @@ fn continue_to_title_screen(mut next_screen: ResMut<NextState<Screen>>) {
next_screen.set(Screen::Title);
}
fn all_assets_loaded(
const fn all_assets_loaded(
interaction_assets: Option<Res<InteractionAssets>>,
credits_music: Option<Res<CreditsMusic>>,
gameplay_music: Option<Res<GameplayMusic>>,

View File

@ -101,7 +101,7 @@ impl UiImageFadeInOut {
let fade = self.fade_duration / self.total_duration;
// Regular trapezoid-shaped graph, flat at the top with alpha = 1.0.
((1.0 - (2.0 * t - 1.0).abs()) / fade).min(1.0)
((1.0 - 2.0f32.mul_add(t, -1.0).abs()) / fade).min(1.0)
}
}

104
src/theme/colorscheme.rs Normal file
View File

@ -0,0 +1,104 @@
use bevy::prelude::*;
use std::ops::Deref;
/// A trait for types that can be converted to a Bevy `Color`.
///
/// Implementing this trait allows a type to be easily converted to various Bevy color types.
///
/// # Examples
///
/// ```
/// use bevy::prelude::*;
/// use maze_ascension::theme::prelude::ColorScheme;
///
/// struct MyColor(u8, u8, u8);
///
/// impl ColorScheme for MyColor {
/// fn to_color(&self) -> Color {
/// Color::srgb(
/// self.0 as f32 / 255.0,
/// self.1 as f32 / 255.0,
/// self.2 as f32 / 255.0
/// )
/// }
/// }
///
/// let my_color = MyColor(255, 0, 0);
/// let bevy_color: Color = my_color.to_color();
/// assert_eq!(bevy_color, Color::srgb(1., 0., 0.));
/// ```
pub trait ColorScheme {
/// Converts the implementing type to a Bevy `Color`.
fn to_color(&self) -> Color;
/// Converts the implementing type to a Bevy `LinearRgba`.
///
/// This method provides a default implementation based on `to_color()`.
fn to_linear_rgba(&self) -> LinearRgba {
self.to_color().to_linear()
}
/// Converts the implementing type to a Bevy `StandardMaterial`.
///
/// This method provides a default implementation that sets the emissive color.
fn to_standart_material(&self) -> StandardMaterial {
StandardMaterial {
emissive: self.to_linear_rgba(),
..default()
}
}
}
/// A wrapper type that implements `From` traits for types implementing `ColorScheme`.
///
/// This wrapper allows for easy conversion from `ColorScheme` types to Bevy color types.
///
/// # Examples
///
/// ```
/// use bevy::prelude::*;
/// use maze_ascension::theme::prelude::{ColorScheme, ColorSchemeWrapper};
///
/// struct MyColor(u8, u8, u8);
///
/// impl ColorScheme for MyColor {
/// fn to_color(&self) -> Color {
/// Color::srgb(
/// self.0 as f32 / 255.0,
/// self.1 as f32 / 255.0,
/// self.2 as f32 / 255.0
/// )
/// }
/// }
///
/// let my_color = MyColor(0, 255, 0);
/// let wrapper = ColorSchemeWrapper(my_color);
/// let bevy_color: Color = wrapper.into();
/// assert_eq!(bevy_color, Color::srgb(0., 1., 0.));
/// ```
pub struct ColorSchemeWrapper<T: ColorScheme>(pub T);
impl<T: ColorScheme> From<T> for ColorSchemeWrapper<T> {
fn from(value: T) -> Self {
Self(value)
}
}
impl<T: ColorScheme> Deref for ColorSchemeWrapper<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<T: ColorScheme> From<ColorSchemeWrapper<T>> for Color {
fn from(value: ColorSchemeWrapper<T>) -> Self {
value.to_color()
}
}
impl<T: ColorScheme> From<ColorSchemeWrapper<T>> for LinearRgba {
fn from(value: ColorSchemeWrapper<T>) -> Self {
value.to_linear_rgba()
}
}

View File

@ -2,6 +2,7 @@
// Unused utilities may trigger this lints undesirably.
mod colorscheme;
pub mod interaction;
pub mod palette;
mod widgets;
@ -9,6 +10,7 @@ mod widgets;
#[allow(unused_imports)]
pub mod prelude {
pub use super::{
colorscheme::{ColorScheme, ColorSchemeWrapper},
interaction::{InteractionPalette, OnPress},
palette as ui_palette,
widgets::{Containers as _, Widgets as _},

View File

@ -11,10 +11,12 @@ 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.;
pub(super) const fn rgb_u8(red: u8, green: u8, blue: u8) -> Color {
Color::srgb(scale(red), scale(green), scale(blue))
}
const fn scale(value: u8) -> f32 {
value as f32 / 255.
value as f32 / MAX_COLOR_VALUE
}

View File

@ -1,18 +1,45 @@
use super::rgb_u8;
use crate::theme::prelude::ColorScheme;
use bevy::prelude::*;
use strum::EnumIter;
pub const BASE: Color = rgb_u8(25, 23, 36);
pub const SURFACE: Color = rgb_u8(31, 29, 46);
pub const OVERLAY: Color = rgb_u8(38, 35, 58);
pub const MUTED: Color = rgb_u8(110, 106, 134);
pub const SUBTLE: Color = rgb_u8(144, 140, 170);
pub const TEXT: Color = rgb_u8(224, 222, 244);
pub const LOVE: Color = rgb_u8(235, 111, 146);
pub const GOLD: Color = rgb_u8(246, 193, 119);
pub const ROSE: Color = rgb_u8(235, 188, 186);
pub const PINE: Color = rgb_u8(49, 116, 143);
pub const FOAM: Color = rgb_u8(156, 207, 216);
pub const IRIS: Color = rgb_u8(196, 167, 231);
pub const HIGHLIGHT_LOW: Color = rgb_u8(33, 32, 46);
pub const HIGHLIGHT_MED: Color = rgb_u8(64, 61, 82);
pub const HIGHLIGHT_HIGH: Color = rgb_u8(82, 79, 103);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EnumIter)]
pub enum RosePine {
Base,
Surface,
Overlay,
Muted,
Subtle,
Text,
Love,
Gold,
Rose,
Pine,
Foam,
Iris,
HighlightLow,
HighlightMed,
HighlightHigh,
}
impl ColorScheme for RosePine {
fn to_color(&self) -> Color {
match self {
Self::Base => rgb_u8(25, 23, 36),
Self::Surface => rgb_u8(31, 29, 46),
Self::Overlay => rgb_u8(38, 35, 58),
Self::Muted => rgb_u8(110, 106, 134),
Self::Subtle => rgb_u8(144, 140, 170),
Self::Text => rgb_u8(224, 222, 244),
Self::Love => rgb_u8(235, 111, 146),
Self::Gold => rgb_u8(246, 193, 119),
Self::Rose => rgb_u8(235, 188, 186),
Self::Pine => rgb_u8(49, 116, 143),
Self::Foam => rgb_u8(156, 207, 216),
Self::Iris => rgb_u8(196, 167, 231),
Self::HighlightLow => rgb_u8(33, 32, 46),
Self::HighlightMed => rgb_u8(64, 61, 82),
Self::HighlightHigh => rgb_u8(82, 79, 103),
}
}
}