mirror of
https://github.com/kristoferssolo/maze-ascension.git
synced 2025-10-21 19:20:34 +00:00
Merge pull request #31 from kristoferssolo/fix/floor-position-overlap
This commit is contained in:
commit
3abf8e2331
10
Cargo.lock
generated
10
Cargo.lock
generated
@ -1614,6 +1614,12 @@ version = "0.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "claims"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bba18ee93d577a8428902687bcc2b6b45a56b1981a1f6d779731c86cc4c5db18"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clang-sys"
|
name = "clang-sys"
|
||||||
version = "1.8.1"
|
version = "1.8.1"
|
||||||
@ -3129,16 +3135,18 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "maze-ascension"
|
name = "maze-ascension"
|
||||||
version = "1.0.2"
|
version = "1.0.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bevy",
|
"bevy",
|
||||||
"bevy-inspector-egui",
|
"bevy-inspector-egui",
|
||||||
"bevy_egui",
|
"bevy_egui",
|
||||||
|
"claims",
|
||||||
"hexlab",
|
"hexlab",
|
||||||
"hexx",
|
"hexx",
|
||||||
"log",
|
"log",
|
||||||
"rand",
|
"rand",
|
||||||
|
"rayon",
|
||||||
"rstest",
|
"rstest",
|
||||||
"rstest_reuse",
|
"rstest_reuse",
|
||||||
"strum",
|
"strum",
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "maze-ascension"
|
name = "maze-ascension"
|
||||||
authors = ["Kristofers Solo <dev@kristofers.xyz>"]
|
authors = ["Kristofers Solo <dev@kristofers.xyz>"]
|
||||||
version = "1.0.2"
|
version = "1.0.3"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
@ -26,6 +26,8 @@ anyhow = "1"
|
|||||||
strum = { version = "0.26", features = ["derive"] }
|
strum = { version = "0.26", features = ["derive"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
claims = "0.8.0"
|
||||||
|
rayon = "1.10.0"
|
||||||
rstest = "0.24"
|
rstest = "0.24"
|
||||||
rstest_reuse = "0.7"
|
rstest_reuse = "0.7"
|
||||||
test-log = { version = "0.2.16", default-features = false, features = [
|
test-log = { version = "0.2.16", default-features = false, features = [
|
||||||
|
|||||||
6
justfile
6
justfile
@ -12,7 +12,7 @@ native-release:
|
|||||||
|
|
||||||
# Run web dev
|
# Run web dev
|
||||||
web-dev:
|
web-dev:
|
||||||
RUST_BACKTRACE=full trunk serve
|
RUSTC_WRAPPER=sccache RUST_BACKTRACE=full trunk serve
|
||||||
|
|
||||||
# Run web release
|
# Run web release
|
||||||
web-release:
|
web-release:
|
||||||
@ -20,8 +20,8 @@ web-release:
|
|||||||
|
|
||||||
# Run tests
|
# Run tests
|
||||||
test:
|
test:
|
||||||
RUSTC_WRAPPER=sccache RUST_BACKTRACE=full cargo test --doc --locked --workspace --no-default-features
|
RUSTC_WRAPPER=sccache cargo test --doc --locked --workspace --no-default-features
|
||||||
RUSTC_WRAPPER=sccache RUST_BACKTRACE=full cargo nextest run --no-default-features --all-targets
|
RUSTC_WRAPPER=sccache cargo nextest run --no-default-features --all-targets
|
||||||
|
|
||||||
# Run CI localy
|
# Run CI localy
|
||||||
ci:
|
ci:
|
||||||
|
|||||||
@ -4,7 +4,7 @@ use crate::{
|
|||||||
components::{CurrentFloor, Floor, FloorYTarget},
|
components::{CurrentFloor, Floor, FloorYTarget},
|
||||||
events::TransitionFloor,
|
events::TransitionFloor,
|
||||||
},
|
},
|
||||||
maze::components::HexMaze,
|
maze::components::{HexMaze, MazeConfig},
|
||||||
player::components::{MovementSpeed, Player},
|
player::components::{MovementSpeed, Player},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -18,13 +18,22 @@ use bevy::prelude::*;
|
|||||||
/// - Removes FloorYTarget component when floor reaches destination
|
/// - Removes FloorYTarget component when floor reaches destination
|
||||||
pub fn move_floors(
|
pub fn move_floors(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
mut maze_query: Query<(Entity, &mut Transform, &FloorYTarget), With<FloorYTarget>>,
|
mut maze_query: Query<
|
||||||
|
(
|
||||||
|
Entity,
|
||||||
|
&mut Transform,
|
||||||
|
&FloorYTarget,
|
||||||
|
&MazeConfig,
|
||||||
|
Has<CurrentFloor>,
|
||||||
|
),
|
||||||
|
With<FloorYTarget>,
|
||||||
|
>,
|
||||||
player_query: Query<&MovementSpeed, With<Player>>,
|
player_query: Query<&MovementSpeed, With<Player>>,
|
||||||
time: Res<Time>,
|
time: Res<Time>,
|
||||||
) {
|
) {
|
||||||
let speed = player_query.get_single().map_or(100., |s| s.0);
|
let speed = player_query.get_single().map_or(100., |s| s.0);
|
||||||
let movement_distance = speed * time.delta_secs();
|
let movement_distance = speed * time.delta_secs();
|
||||||
for (entity, mut transform, movement_state) in maze_query.iter_mut() {
|
for (entity, mut transform, movement_state, config, is_current_floor) in maze_query.iter_mut() {
|
||||||
let delta = movement_state.0 - transform.translation.y;
|
let delta = movement_state.0 - transform.translation.y;
|
||||||
if delta.abs() > MOVEMENT_THRESHOLD {
|
if delta.abs() > MOVEMENT_THRESHOLD {
|
||||||
let movement = delta.signum() * movement_distance.min(delta.abs());
|
let movement = delta.signum() * movement_distance.min(delta.abs());
|
||||||
@ -32,6 +41,13 @@ pub fn move_floors(
|
|||||||
} else {
|
} else {
|
||||||
transform.translation.y = movement_state.0;
|
transform.translation.y = movement_state.0;
|
||||||
commands.entity(entity).remove::<FloorYTarget>();
|
commands.entity(entity).remove::<FloorYTarget>();
|
||||||
|
if is_current_floor {
|
||||||
|
info!("Current floor seed: {}", config.seed);
|
||||||
|
info!(
|
||||||
|
"Start pos: (q={}, r={}). End pos: (q={}, r={})",
|
||||||
|
config.start_pos.x, config.start_pos.y, config.end_pos.x, config.end_pos.y,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,11 +32,7 @@ pub fn spawn_floor(
|
|||||||
|
|
||||||
commands.queue(SpawnMaze {
|
commands.queue(SpawnMaze {
|
||||||
floor: target_floor,
|
floor: target_floor,
|
||||||
config: MazeConfig {
|
config: MazeConfig::from_self(config),
|
||||||
start_pos: config.end_pos,
|
|
||||||
radius: config.radius + 1,
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
//! Module defines the core components and configuration structures used
|
//! Module defines the core components and configuration structures used
|
||||||
//! for maze generation and rendering, including hexagonal maze layouts,
|
//! for maze generation and rendering, including hexagonal maze layouts,
|
||||||
//! tiles, walls, and maze configuration.
|
//! tiles, walls, and maze configuration.
|
||||||
use super::GlobalMazeConfig;
|
use super::{coordinates::is_within_radius, GlobalMazeConfig};
|
||||||
use crate::floor::components::Floor;
|
use crate::floor::components::Floor;
|
||||||
|
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
@ -49,31 +49,19 @@ impl MazeConfig {
|
|||||||
radius: u16,
|
radius: u16,
|
||||||
orientation: HexOrientation,
|
orientation: HexOrientation,
|
||||||
seed: Option<u64>,
|
seed: Option<u64>,
|
||||||
global_conig: &GlobalMazeConfig,
|
global_config: &GlobalMazeConfig,
|
||||||
start_pos: Option<Hex>,
|
start_pos: Option<Hex>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let seed = seed.unwrap_or_else(|| thread_rng().gen());
|
let (seed, mut rng) = setup_rng(seed);
|
||||||
let mut rng = StdRng::seed_from_u64(seed);
|
|
||||||
|
|
||||||
let start_pos = start_pos.unwrap_or_else(|| generate_pos(radius, &mut rng));
|
let start_pos = start_pos.unwrap_or_else(|| generate_pos(radius, &mut rng));
|
||||||
|
|
||||||
// Generate end position ensuring start and end are different
|
// Generate end position ensuring start and end are different
|
||||||
let mut end_pos;
|
let end_pos = generate_end_pos(radius, start_pos, &mut rng);
|
||||||
loop {
|
|
||||||
end_pos = generate_pos(radius, &mut rng);
|
|
||||||
if start_pos != end_pos {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
info!(
|
|
||||||
"Start pos: (q={}, r={}). End pos: (q={}, r={})",
|
|
||||||
start_pos.x, start_pos.y, end_pos.x, end_pos.y
|
|
||||||
);
|
|
||||||
|
|
||||||
let layout = HexLayout {
|
let layout = HexLayout {
|
||||||
orientation,
|
orientation,
|
||||||
hex_size: Vec2::splat(global_conig.hex_size),
|
hex_size: Vec2::splat(global_config.hex_size),
|
||||||
..default()
|
..default()
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -86,12 +74,35 @@ impl MazeConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn from_self(config: &Self) -> Self {
|
||||||
|
let start_pos = config.end_pos;
|
||||||
|
let (seed, mut rng) = setup_rng(None);
|
||||||
|
|
||||||
|
let end_pos = generate_end_pos(config.radius, start_pos, &mut rng);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
radius: config.radius + 1,
|
||||||
|
start_pos,
|
||||||
|
end_pos,
|
||||||
|
seed,
|
||||||
|
layout: config.layout.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Updates the maze configuration with new global settings.
|
/// Updates the maze configuration with new global settings.
|
||||||
pub fn update(&mut self, global_conig: &GlobalMazeConfig) {
|
pub fn update(&mut self, global_conig: &GlobalMazeConfig) {
|
||||||
self.layout.hex_size = Vec2::splat(global_conig.hex_size);
|
self.layout.hex_size = Vec2::splat(global_conig.hex_size);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TO
|
||||||
|
// 3928551514041614914
|
||||||
|
// (4, 0)
|
||||||
|
|
||||||
|
// FROM
|
||||||
|
// 7365371276044996661
|
||||||
|
// ()
|
||||||
|
|
||||||
impl Default for MazeConfig {
|
impl Default for MazeConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::new(
|
Self::new(
|
||||||
@ -104,21 +115,38 @@ impl Default for MazeConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn setup_rng(seed: Option<u64>) -> (u64, StdRng) {
|
||||||
|
let seed = seed.unwrap_or_else(|| thread_rng().gen());
|
||||||
|
let rng = StdRng::seed_from_u64(seed);
|
||||||
|
(seed, rng)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_end_pos<R: Rng>(radius: u16, start_pos: Hex, rng: &mut R) -> Hex {
|
||||||
|
let mut end_pos;
|
||||||
|
loop {
|
||||||
|
end_pos = generate_pos(radius, rng);
|
||||||
|
if start_pos != end_pos {
|
||||||
|
return end_pos;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Generates a random position within a hexagonal radius.
|
/// Generates a random position within a hexagonal radius.
|
||||||
///
|
///
|
||||||
/// # Returns
|
/// # Returns
|
||||||
/// A valid Hex coordinate within the specified radius
|
/// A valid Hex coordinate within the specified radius
|
||||||
fn generate_pos<R: Rng>(radius: u16, rng: &mut R) -> Hex {
|
fn generate_pos<R: Rng>(radius: u16, rng: &mut R) -> Hex {
|
||||||
let radius = radius as i32;
|
let radius = radius as i32;
|
||||||
loop {
|
|
||||||
let q = 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)
|
|
||||||
|
|
||||||
// Check if the position is within the hexagonal radius
|
loop {
|
||||||
// Using the formula: max(abs(q), abs(r), abs(s)) <= radius
|
// Generate coordinates using cube coordinate bounds
|
||||||
if q.abs().max(r.abs()).max(s.abs()) <= radius {
|
let q = rng.gen_range(-radius..=radius);
|
||||||
return Hex::new(q, r);
|
let r = rng.gen_range((-radius).max(-q - radius)..=radius.min(-q + radius));
|
||||||
|
|
||||||
|
if let Ok(is_valid) = is_within_radius(radius, &(q, r)) {
|
||||||
|
if is_valid {
|
||||||
|
return Hex::new(q, r);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -126,20 +154,9 @@ fn generate_pos<R: Rng>(radius: u16, rng: &mut R) -> Hex {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use claims::*;
|
||||||
use rstest::*;
|
use rstest::*;
|
||||||
|
|
||||||
fn is_within_radius(hex: Hex, radius: u16) -> bool {
|
|
||||||
let q = hex.x;
|
|
||||||
let r = hex.y;
|
|
||||||
let s = -q - r;
|
|
||||||
q.abs().max(r.abs()).max(s.abs()) <= radius as i32
|
|
||||||
}
|
|
||||||
|
|
||||||
#[fixture]
|
|
||||||
fn test_radius() -> Vec<u16> {
|
|
||||||
vec![1, 2, 5, 8]
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
#[case(1)]
|
#[case(1)]
|
||||||
#[case(2)]
|
#[case(2)]
|
||||||
@ -156,18 +173,8 @@ mod tests {
|
|||||||
assert_eq!(config.seed, 12345);
|
assert_eq!(config.seed, 12345);
|
||||||
assert_eq!(config.layout.orientation, orientation);
|
assert_eq!(config.layout.orientation, orientation);
|
||||||
|
|
||||||
assert!(
|
assert_ok!(is_within_radius(radius, &config.start_pos),);
|
||||||
is_within_radius(config.start_pos, radius),
|
assert_ok!(is_within_radius(radius, &config.end_pos));
|
||||||
"Start pos {:?} outside radius {}",
|
|
||||||
config.start_pos,
|
|
||||||
radius
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
is_within_radius(config.end_pos, radius),
|
|
||||||
"End pos {:?} outside radius {}",
|
|
||||||
config.end_pos,
|
|
||||||
radius
|
|
||||||
);
|
|
||||||
assert_ne!(config.start_pos, config.end_pos);
|
assert_ne!(config.start_pos, config.end_pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -178,13 +185,13 @@ mod tests {
|
|||||||
let config = MazeConfig::default();
|
let config = MazeConfig::default();
|
||||||
let radius = config.radius;
|
let radius = config.radius;
|
||||||
|
|
||||||
assert!(is_within_radius(config.start_pos, radius));
|
assert_ok!(is_within_radius(radius, &config.start_pos));
|
||||||
assert!(is_within_radius(config.end_pos, radius));
|
assert_ok!(is_within_radius(radius, &config.end_pos));
|
||||||
assert_ne!(config.start_pos, config.end_pos);
|
assert_ne!(config.start_pos, config.end_pos);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rstest]
|
#[test]
|
||||||
fn maze_config_default_with_seeds() {
|
fn maze_config_default_with_seeds() {
|
||||||
let test_seeds = [
|
let test_seeds = [
|
||||||
None,
|
None,
|
||||||
@ -206,8 +213,8 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(config.radius, 8);
|
assert_eq!(config.radius, 8);
|
||||||
assert_eq!(config.layout.orientation, HexOrientation::Flat);
|
assert_eq!(config.layout.orientation, HexOrientation::Flat);
|
||||||
assert!(is_within_radius(config.start_pos, 8));
|
assert_ok!(is_within_radius(8, &config.start_pos));
|
||||||
assert!(is_within_radius(config.end_pos, 8));
|
assert_ok!(is_within_radius(8, &config.end_pos));
|
||||||
assert_ne!(config.start_pos, config.end_pos);
|
assert_ne!(config.start_pos, config.end_pos);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -238,12 +245,7 @@ mod tests {
|
|||||||
|
|
||||||
for _ in 0..10 {
|
for _ in 0..10 {
|
||||||
let pos = generate_pos(radius, &mut rng);
|
let pos = generate_pos(radius, &mut rng);
|
||||||
assert!(
|
assert_ok!(is_within_radius(radius, &pos),);
|
||||||
is_within_radius(pos, radius),
|
|
||||||
"Position {:?} outside radius {}",
|
|
||||||
pos,
|
|
||||||
radius
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -317,4 +319,51 @@ mod tests {
|
|||||||
assert_eq!(config.layout.hex_size.x, 0.0);
|
assert_eq!(config.layout.hex_size.x, 0.0);
|
||||||
assert_eq!(config.layout.hex_size.y, 0.0);
|
assert_eq!(config.layout.hex_size.y, 0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn basic_generation() {
|
||||||
|
let mut rng = thread_rng();
|
||||||
|
let radius = 2;
|
||||||
|
let hex = generate_pos(radius, &mut rng);
|
||||||
|
|
||||||
|
// Test that generated position is within radius
|
||||||
|
assert_ok!(is_within_radius(radius as i32, &(hex.x, hex.y)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case(1)]
|
||||||
|
#[case(2)]
|
||||||
|
#[case(3)]
|
||||||
|
#[case(6)]
|
||||||
|
fn multiple_radii(#[case] radius: u16) {
|
||||||
|
let mut rng = thread_rng();
|
||||||
|
|
||||||
|
// Generate multiple points for each radius
|
||||||
|
for _ in 0..100 {
|
||||||
|
let hex = generate_pos(radius, &mut rng);
|
||||||
|
assert_ok!(is_within_radius(radius, &hex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn zero_radius() {
|
||||||
|
let mut rng = thread_rng();
|
||||||
|
let hex = generate_pos(0, &mut rng);
|
||||||
|
|
||||||
|
// With radius 0, only (0,0) should be possible
|
||||||
|
assert_eq!(hex.x, 0);
|
||||||
|
assert_eq!(hex.y, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn large_radius() {
|
||||||
|
let mut rng = thread_rng();
|
||||||
|
let radius = 100;
|
||||||
|
let iterations = 100;
|
||||||
|
|
||||||
|
for _ in 0..iterations {
|
||||||
|
let hex = generate_pos(radius, &mut rng);
|
||||||
|
assert_ok!(is_within_radius(radius, &hex));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
139
src/maze/coordinates.rs
Normal file
139
src/maze/coordinates.rs
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
use hexx::Hex;
|
||||||
|
|
||||||
|
use super::errors::RadiusError;
|
||||||
|
|
||||||
|
pub trait Coordinates {
|
||||||
|
fn get_coords(&self) -> (i32, i32);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Coordinates for (i32, i32) {
|
||||||
|
fn get_coords(&self) -> (i32, i32) {
|
||||||
|
*self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Coordinates for Hex {
|
||||||
|
fn get_coords(&self) -> (i32, i32) {
|
||||||
|
(self.x, self.y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_within_radius<R, C>(radius: R, coords: &C) -> Result<bool, RadiusError>
|
||||||
|
where
|
||||||
|
R: Into<i32>,
|
||||||
|
C: Coordinates,
|
||||||
|
{
|
||||||
|
let radius = radius.into();
|
||||||
|
|
||||||
|
if radius < 0 {
|
||||||
|
return Err(RadiusError::NegativeRadius(radius));
|
||||||
|
}
|
||||||
|
|
||||||
|
let (q, r) = coords.get_coords();
|
||||||
|
let s = -q - r; // Calculate third axial coordinate (q + r + s = 0)
|
||||||
|
|
||||||
|
Ok(q.abs().max(r.abs()).max(s.abs()) <= radius)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use claims::*;
|
||||||
|
use rstest::*;
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
// Original test cases
|
||||||
|
#[case(0, (0, 0), true)] // Center point
|
||||||
|
#[case(1, (1, 0), true)] // Point at radius 1
|
||||||
|
#[case(1, (2, 0), false)] // Point outside radius 1
|
||||||
|
#[case(2, (2, 0), true)] // East
|
||||||
|
#[case(2, (0, 2), true)] // Southeast
|
||||||
|
#[case(2, (-2, 2), true)] // Southwest
|
||||||
|
#[case(2, (-2, 0), true)] // West
|
||||||
|
#[case(2, (0, -2), true)] // Northwest
|
||||||
|
#[case(2, (2, -2), true)] // Northeast
|
||||||
|
#[case(2, (3, 0), false)] // Just outside radius 2
|
||||||
|
// Large radius test cases
|
||||||
|
#[case(6, (6, 0), true)] // East at radius 6
|
||||||
|
#[case(6, (0, 6), true)] // Southeast at radius 6
|
||||||
|
#[case(6, (-6, 6), true)] // Southwest at radius 6
|
||||||
|
#[case(6, (-6, 0), true)] // West at radius 6
|
||||||
|
#[case(6, (0, -6), true)] // Northwest at radius 6
|
||||||
|
#[case(6, (6, -6), true)] // Northeast at radius 6
|
||||||
|
#[case(6, (7, 0), false)] // Just outside radius 6 east
|
||||||
|
#[case(6, (4, 4), false)] // Outside radius 6 diagonal
|
||||||
|
#[case(6, (5, 5), false)] // Outside radius 6 diagonal
|
||||||
|
// Edge cases with large radius
|
||||||
|
#[case(6, (6, -3), true)] // Complex position within radius 6
|
||||||
|
#[case(6, (-3, 6), true)] // Complex position within radius 6
|
||||||
|
#[case(6, (3, -6), true)] // Complex position within radius 6
|
||||||
|
#[case(6, (7, -7), false)] // Outside radius 6 corner
|
||||||
|
fn valid_radius_tuple(#[case] radius: i32, #[case] pos: (i32, i32), #[case] expected: bool) {
|
||||||
|
let result = is_within_radius(radius, &pos);
|
||||||
|
assert_ok_eq!(result, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
// Large radius test cases for Hex struct
|
||||||
|
#[case(6, (6, 0), true)] // East at radius 6
|
||||||
|
#[case(6, (0, 6), true)] // Southeast at radius 6
|
||||||
|
#[case(6, (-6, 6), true)] // Southwest at radius 6
|
||||||
|
#[case(6, (-6, 0), true)] // West at radius 6
|
||||||
|
#[case(6, (0, -6), true)] // Northwest at radius 6
|
||||||
|
#[case(6, (6, -6), true)] // Northeast at radius 6
|
||||||
|
#[case(6, (4, 4), false)] // Outside radius 6 diagonal
|
||||||
|
#[case(6, (5, 5), false)] // Outside radius 6 diagonal
|
||||||
|
fn valid_radius_hex(#[case] radius: i32, #[case] pos: (i32, i32), #[case] expected: bool) {
|
||||||
|
let hex = Hex::from(pos);
|
||||||
|
let result = is_within_radius(radius, &hex);
|
||||||
|
assert_ok_eq!(result, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case(-1)]
|
||||||
|
#[case(-2)]
|
||||||
|
#[case(-5)]
|
||||||
|
fn negative_radius(#[case] radius: i32) {
|
||||||
|
let result = is_within_radius(radius, &(0, 0));
|
||||||
|
assert_err!(&result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn boundary_points() {
|
||||||
|
let radius = 3;
|
||||||
|
// Test points exactly on the boundary of radius 3
|
||||||
|
assert_ok_eq!(is_within_radius(radius, &(3, 0)), true); // East boundary
|
||||||
|
assert_ok_eq!(is_within_radius(radius, &(0, 3)), true); // Southeast boundary
|
||||||
|
assert_ok_eq!(is_within_radius(radius, &(-3, 3)), true); // Southwest boundary
|
||||||
|
assert_ok_eq!(is_within_radius(radius, &(-3, 0)), true); // West boundary
|
||||||
|
assert_ok_eq!(is_within_radius(radius, &(0, -3)), true); // Northwest boundary
|
||||||
|
assert_ok_eq!(is_within_radius(radius, &(3, -3)), true); // Northeast boundary
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn large_boundary_points() {
|
||||||
|
let radius = 6;
|
||||||
|
// Test points exactly on the boundary of radius 6
|
||||||
|
assert_ok_eq!(is_within_radius(radius, &(6, 0)), true); // East boundary
|
||||||
|
assert_ok_eq!(is_within_radius(radius, &(0, 6)), true); // Southeast boundary
|
||||||
|
assert_ok_eq!(is_within_radius(radius, &(-6, 6)), true); // Southwest boundary
|
||||||
|
assert_ok_eq!(is_within_radius(radius, &(-6, 0)), true); // West boundary
|
||||||
|
assert_ok_eq!(is_within_radius(radius, &(0, -6)), true); // Northwest boundary
|
||||||
|
assert_ok_eq!(is_within_radius(radius, &(6, -6)), true); // Northeast boundary
|
||||||
|
|
||||||
|
// Test points just outside the boundary
|
||||||
|
assert_ok_eq!(is_within_radius(radius, &(7, 0)), false); // Just outside east
|
||||||
|
assert_ok_eq!(is_within_radius(radius, &(0, 7)), false); // Just outside southeast
|
||||||
|
assert_ok_eq!(is_within_radius(radius, &(-7, 7)), false); // Just outside southwest
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn different_coordinate_types() {
|
||||||
|
// Test with tuple coordinates
|
||||||
|
assert_ok_eq!(is_within_radius(2, &(1, 1)), true);
|
||||||
|
|
||||||
|
// Test with Hex struct
|
||||||
|
let hex = Hex { x: 1, y: 1 };
|
||||||
|
assert_ok_eq!(is_within_radius(2, &hex), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -25,7 +25,11 @@ pub enum MazeError {
|
|||||||
Other(#[from] anyhow::Error),
|
Other(#[from] anyhow::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type MazeResult<T> = Result<T, MazeError>;
|
#[derive(Debug, Error)]
|
||||||
|
pub enum RadiusError {
|
||||||
|
#[error("Radius cannot be negative: {0}")]
|
||||||
|
NegativeRadius(i32),
|
||||||
|
}
|
||||||
|
|
||||||
impl MazeError {
|
impl MazeError {
|
||||||
pub fn config_error(msg: impl Into<String>) -> Self {
|
pub fn config_error(msg: impl Into<String>) -> Self {
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
mod assets;
|
mod assets;
|
||||||
pub mod commands;
|
pub mod commands;
|
||||||
pub mod components;
|
pub mod components;
|
||||||
|
pub mod coordinates;
|
||||||
pub mod errors;
|
pub mod errors;
|
||||||
pub mod resources;
|
pub mod resources;
|
||||||
mod systems;
|
mod systems;
|
||||||
|
|||||||
@ -68,6 +68,7 @@ pub fn spawn_maze(
|
|||||||
.id();
|
.id();
|
||||||
|
|
||||||
let assets = MazeAssets::new(&mut meshes, &mut materials, &global_config);
|
let assets = MazeAssets::new(&mut meshes, &mut materials, &global_config);
|
||||||
|
|
||||||
spawn_maze_tiles(
|
spawn_maze_tiles(
|
||||||
&mut commands,
|
&mut commands,
|
||||||
entity,
|
entity,
|
||||||
|
|||||||
@ -16,7 +16,7 @@ pub struct MovementSpeed(pub f32);
|
|||||||
|
|
||||||
impl Default for MovementSpeed {
|
impl Default for MovementSpeed {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self(100.)
|
Self(200.)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -7,7 +7,7 @@ use crate::{
|
|||||||
hint::assets::HintAssets,
|
hint::assets::HintAssets,
|
||||||
player::assets::PlayerAssets,
|
player::assets::PlayerAssets,
|
||||||
screens::Screen,
|
screens::Screen,
|
||||||
theme::{interaction::InteractionAssets, prelude::*},
|
theme::{assets::InteractionAssets, prelude::*},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn plugin(app: &mut App) {
|
pub fn plugin(app: &mut App) {
|
||||||
|
|||||||
24
src/theme/assets.rs
Normal file
24
src/theme/assets.rs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Resource, Asset, Reflect, Clone)]
|
||||||
|
pub struct InteractionAssets {
|
||||||
|
#[dependency]
|
||||||
|
pub(super) hover: Handle<AudioSource>,
|
||||||
|
#[dependency]
|
||||||
|
pub(super) press: Handle<AudioSource>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InteractionAssets {
|
||||||
|
pub const PATH_BUTTON_HOVER: &'static str = "audio/sound_effects/button_hover.ogg";
|
||||||
|
pub const PATH_BUTTON_PRESS: &'static str = "audio/sound_effects/button_press.ogg";
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromWorld for InteractionAssets {
|
||||||
|
fn from_world(world: &mut World) -> Self {
|
||||||
|
let assets = world.resource::<AssetServer>();
|
||||||
|
Self {
|
||||||
|
hover: assets.load(Self::PATH_BUTTON_HOVER),
|
||||||
|
press: assets.load(Self::PATH_BUTTON_PRESS),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/theme/components.rs
Normal file
16
src/theme/components.rs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
/// Palette for widget interactions. Add this to an entity that supports
|
||||||
|
/// [`Interaction`]s, such as a button, to change its [`BackgroundColor`] based
|
||||||
|
/// on the current interaction state.
|
||||||
|
#[derive(Component, Debug, Reflect)]
|
||||||
|
#[reflect(Component)]
|
||||||
|
pub struct InteractionPalette {
|
||||||
|
pub none: Color,
|
||||||
|
pub hovered: Color,
|
||||||
|
pub pressed: Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Reflect, Component)]
|
||||||
|
#[reflect(Component)]
|
||||||
|
pub struct UrlLink(pub String);
|
||||||
6
src/theme/events.rs
Normal file
6
src/theme/events.rs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
/// Event triggered on a UI entity when the [`Interaction`] component on the same entity changes to
|
||||||
|
/// [`Interaction::Pressed`]. Observe this event to detect e.g. button presses.
|
||||||
|
#[derive(Event)]
|
||||||
|
pub struct OnPress;
|
||||||
@ -1,102 +0,0 @@
|
|||||||
use bevy::prelude::*;
|
|
||||||
|
|
||||||
use crate::{asset_tracking::LoadResource, audio::SoundEffect};
|
|
||||||
|
|
||||||
pub(super) fn plugin(app: &mut App) {
|
|
||||||
app.register_type::<InteractionPalette>();
|
|
||||||
app.load_resource::<InteractionAssets>();
|
|
||||||
app.add_systems(
|
|
||||||
Update,
|
|
||||||
(
|
|
||||||
trigger_on_press,
|
|
||||||
apply_interaction_palette,
|
|
||||||
trigger_interaction_sound_effect,
|
|
||||||
)
|
|
||||||
.run_if(resource_exists::<InteractionAssets>),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Palette for widget interactions. Add this to an entity that supports
|
|
||||||
/// [`Interaction`]s, such as a button, to change its [`BackgroundColor`] based
|
|
||||||
/// on the current interaction state.
|
|
||||||
#[derive(Component, Debug, Reflect)]
|
|
||||||
#[reflect(Component)]
|
|
||||||
pub struct InteractionPalette {
|
|
||||||
pub none: Color,
|
|
||||||
pub hovered: Color,
|
|
||||||
pub pressed: Color,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Event triggered on a UI entity when the [`Interaction`] component on the same entity changes to
|
|
||||||
/// [`Interaction::Pressed`]. Observe this event to detect e.g. button presses.
|
|
||||||
#[derive(Event)]
|
|
||||||
pub struct OnPress;
|
|
||||||
|
|
||||||
fn trigger_on_press(
|
|
||||||
interaction_query: Query<(Entity, &Interaction), Changed<Interaction>>,
|
|
||||||
mut commands: Commands,
|
|
||||||
) {
|
|
||||||
for (entity, interaction) in &interaction_query {
|
|
||||||
if matches!(interaction, Interaction::Pressed) {
|
|
||||||
commands.trigger_targets(OnPress, entity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn apply_interaction_palette(
|
|
||||||
mut palette_query: Query<
|
|
||||||
(&Interaction, &InteractionPalette, &mut BackgroundColor),
|
|
||||||
Changed<Interaction>,
|
|
||||||
>,
|
|
||||||
) {
|
|
||||||
for (interaction, palette, mut background) in &mut palette_query {
|
|
||||||
*background = match interaction {
|
|
||||||
Interaction::None => palette.none,
|
|
||||||
Interaction::Hovered => palette.hovered,
|
|
||||||
Interaction::Pressed => palette.pressed,
|
|
||||||
}
|
|
||||||
.into();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Resource, Asset, Reflect, Clone)]
|
|
||||||
pub struct InteractionAssets {
|
|
||||||
#[dependency]
|
|
||||||
hover: Handle<AudioSource>,
|
|
||||||
#[dependency]
|
|
||||||
press: Handle<AudioSource>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl InteractionAssets {
|
|
||||||
pub const PATH_BUTTON_HOVER: &'static str = "audio/sound_effects/button_hover.ogg";
|
|
||||||
pub const PATH_BUTTON_PRESS: &'static str = "audio/sound_effects/button_press.ogg";
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromWorld for InteractionAssets {
|
|
||||||
fn from_world(world: &mut World) -> Self {
|
|
||||||
let assets = world.resource::<AssetServer>();
|
|
||||||
Self {
|
|
||||||
hover: assets.load(Self::PATH_BUTTON_HOVER),
|
|
||||||
press: assets.load(Self::PATH_BUTTON_PRESS),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn trigger_interaction_sound_effect(
|
|
||||||
interaction_query: Query<&Interaction, Changed<Interaction>>,
|
|
||||||
interaction_assets: Res<InteractionAssets>,
|
|
||||||
mut commands: Commands,
|
|
||||||
) {
|
|
||||||
for interaction in &interaction_query {
|
|
||||||
let source = match interaction {
|
|
||||||
Interaction::Hovered => interaction_assets.hover.clone(),
|
|
||||||
Interaction::Pressed => interaction_assets.press.clone(),
|
|
||||||
_ => continue,
|
|
||||||
};
|
|
||||||
commands.spawn((
|
|
||||||
AudioPlayer::<AudioSource>(source),
|
|
||||||
PlaybackSettings::DESPAWN,
|
|
||||||
SoundEffect,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -2,23 +2,33 @@
|
|||||||
|
|
||||||
// Unused utilities may trigger this lints undesirably.
|
// Unused utilities may trigger this lints undesirably.
|
||||||
|
|
||||||
|
pub mod assets;
|
||||||
mod colorscheme;
|
mod colorscheme;
|
||||||
pub mod interaction;
|
pub mod components;
|
||||||
|
pub mod events;
|
||||||
pub mod palette;
|
pub mod palette;
|
||||||
|
mod systems;
|
||||||
mod widgets;
|
mod widgets;
|
||||||
|
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
pub mod prelude {
|
pub mod prelude {
|
||||||
pub use super::{
|
pub use super::{
|
||||||
colorscheme::{ColorScheme, ColorSchemeWrapper},
|
colorscheme::{ColorScheme, ColorSchemeWrapper},
|
||||||
interaction::{InteractionPalette, OnPress},
|
components::{InteractionPalette, UrlLink},
|
||||||
|
events::OnPress,
|
||||||
palette as ui_palette,
|
palette as ui_palette,
|
||||||
widgets::{Containers as _, Widgets as _},
|
widgets::{Containers as _, Widgets as _},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
use assets::InteractionAssets;
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
use prelude::InteractionPalette;
|
||||||
|
|
||||||
|
use crate::asset_tracking::LoadResource;
|
||||||
|
|
||||||
pub(super) fn plugin(app: &mut App) {
|
pub(super) fn plugin(app: &mut App) {
|
||||||
app.add_plugins(interaction::plugin);
|
app.register_type::<InteractionPalette>();
|
||||||
|
app.load_resource::<InteractionAssets>();
|
||||||
|
app.add_plugins(systems::plugin);
|
||||||
}
|
}
|
||||||
|
|||||||
52
src/theme/systems/button.rs
Normal file
52
src/theme/systems/button.rs
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
audio::SoundEffect,
|
||||||
|
theme::{assets::InteractionAssets, events::OnPress, prelude::InteractionPalette},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn trigger_on_press(
|
||||||
|
interaction_query: Query<(Entity, &Interaction), Changed<Interaction>>,
|
||||||
|
mut commands: Commands,
|
||||||
|
) {
|
||||||
|
for (entity, interaction) in &interaction_query {
|
||||||
|
if matches!(interaction, Interaction::Pressed) {
|
||||||
|
commands.trigger_targets(OnPress, entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_interaction_palette(
|
||||||
|
mut palette_query: Query<
|
||||||
|
(&Interaction, &InteractionPalette, &mut BackgroundColor),
|
||||||
|
Changed<Interaction>,
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
for (interaction, palette, mut background) in &mut palette_query {
|
||||||
|
*background = match interaction {
|
||||||
|
Interaction::None => palette.none,
|
||||||
|
Interaction::Hovered => palette.hovered,
|
||||||
|
Interaction::Pressed => palette.pressed,
|
||||||
|
}
|
||||||
|
.into();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn trigger_interaction_sound_effect(
|
||||||
|
interaction_query: Query<&Interaction, Changed<Interaction>>,
|
||||||
|
interaction_assets: Res<InteractionAssets>,
|
||||||
|
mut commands: Commands,
|
||||||
|
) {
|
||||||
|
for interaction in &interaction_query {
|
||||||
|
let source = match interaction {
|
||||||
|
Interaction::Hovered => interaction_assets.hover.clone(),
|
||||||
|
Interaction::Pressed => interaction_assets.press.clone(),
|
||||||
|
_ => continue,
|
||||||
|
};
|
||||||
|
commands.spawn((
|
||||||
|
AudioPlayer::<AudioSource>(source),
|
||||||
|
PlaybackSettings::DESPAWN,
|
||||||
|
SoundEffect,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/theme/systems/mod.rs
Normal file
18
src/theme/systems/mod.rs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
mod button;
|
||||||
|
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use button::{apply_interaction_palette, trigger_interaction_sound_effect, trigger_on_press};
|
||||||
|
|
||||||
|
use super::assets::InteractionAssets;
|
||||||
|
|
||||||
|
pub(super) fn plugin(app: &mut App) {
|
||||||
|
app.add_systems(
|
||||||
|
Update,
|
||||||
|
(
|
||||||
|
trigger_on_press,
|
||||||
|
apply_interaction_palette,
|
||||||
|
trigger_interaction_sound_effect,
|
||||||
|
)
|
||||||
|
.run_if(resource_exists::<InteractionAssets>),
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,10 +1,13 @@
|
|||||||
//! 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::*, window::SystemCursorIcon,
|
||||||
|
winit::cursor::CursorIcon,
|
||||||
|
};
|
||||||
use rose_pine::RosePineDawn;
|
use rose_pine::RosePineDawn;
|
||||||
|
|
||||||
use super::prelude::ColorScheme;
|
use super::prelude::{ColorScheme, InteractionPalette};
|
||||||
use crate::theme::{interaction::InteractionPalette, palette::*};
|
use crate::theme::palette::*;
|
||||||
|
|
||||||
/// An extension trait for spawning UI widgets.
|
/// An extension trait for spawning UI widgets.
|
||||||
pub trait Widgets {
|
pub trait Widgets {
|
||||||
@ -35,6 +38,7 @@ impl<T: SpawnUi> Widgets for T {
|
|||||||
border: UiRect::all(Px(4.)),
|
border: UiRect::all(Px(4.)),
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
|
CursorIcon::System(SystemCursorIcon::Pointer),
|
||||||
BorderRadius::all(Px(8.)),
|
BorderRadius::all(Px(8.)),
|
||||||
BorderColor(RosePineDawn::Text.to_color()),
|
BorderColor(RosePineDawn::Text.to_color()),
|
||||||
InteractionPalette {
|
InteractionPalette {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user