mirror of
https://github.com/kristoferssolo/Qualification-Thesis.git
synced 2026-03-22 00:26:32 +00:00
feat: add more code examples
This commit is contained in:
176
assets/code/hexlab/builder.rs
Normal file
176
assets/code/hexlab/builder.rs
Normal file
@@ -0,0 +1,176 @@
|
||||
/// A builder pattern for creating hexagonal mazes.
|
||||
///
|
||||
/// This struct provides a fluent interface for configuring and building hexagonal mazes.
|
||||
/// It offers flexibility in specifying the maze size, random seed, generation algorithm,
|
||||
/// and starting position.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Basic usage:
|
||||
/// ```
|
||||
/// use hexlab::prelude::*;
|
||||
///
|
||||
/// let maze = MazeBuilder::new()
|
||||
/// .with_radius(5)
|
||||
/// .build()
|
||||
/// .expect("Failed to create maze");
|
||||
///
|
||||
/// // A radius of 5 creates 61 hexagonal tiles
|
||||
/// assert!(!maze.is_empty());
|
||||
/// assert_eq!(maze.len(), 91);
|
||||
/// ```
|
||||
///
|
||||
/// Using a seed for reproducible results:
|
||||
/// ```
|
||||
/// use hexlab::prelude::*;
|
||||
///
|
||||
/// let maze1 = MazeBuilder::new()
|
||||
/// .with_radius(3)
|
||||
/// .with_seed(12345)
|
||||
/// .build()
|
||||
/// .expect("Failed to create maze");
|
||||
///
|
||||
/// let maze2 = MazeBuilder::new()
|
||||
/// .with_radius(3)
|
||||
/// .with_seed(12345)
|
||||
/// .build()
|
||||
/// .expect("Failed to create maze");
|
||||
///
|
||||
/// // Same seed should produce identical mazes
|
||||
/// assert_eq!(maze1.len(), maze2.len());
|
||||
/// assert_eq!(maze1, maze2);
|
||||
/// ```
|
||||
///
|
||||
/// Specifying a custom generator:
|
||||
/// ```
|
||||
/// use hexlab::prelude::*;
|
||||
///
|
||||
/// let maze = MazeBuilder::new()
|
||||
/// .with_radius(7)
|
||||
/// .with_generator(GeneratorType::RecursiveBacktracking)
|
||||
/// .build()
|
||||
/// .expect("Failed to create maze");
|
||||
/// ```
|
||||
#[derive(Default)]
|
||||
pub struct MazeBuilder {
|
||||
radius: Option<u16>,
|
||||
seed: Option<u64>,
|
||||
generator_type: GeneratorType,
|
||||
start_position: Option<Hex>,
|
||||
}
|
||||
|
||||
impl MazeBuilder {
|
||||
/// Creates a new [`MazeBuilder`] instance with default settings.
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Sets the radius for the hexagonal maze.
|
||||
///
|
||||
/// The radius determines the size of the maze, specifically the number of tiles
|
||||
/// from the center (0,0) to the edge of the hexagon, not including the center tile.
|
||||
/// For example, a radius of 3 would create a maze with 3 tiles from center to edge,
|
||||
/// resulting in a total diameter of 7 tiles (3 + 1 + 3).
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `radius` - The number of tiles from the center to the edge of the hexagon.
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub const fn with_radius(mut self, radius: u16) -> Self {
|
||||
self.radius = Some(radius);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the random seed for maze generation.
|
||||
///
|
||||
/// Using the same seed will produce identical mazes, allowing for reproducible results.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `seed` - The random seed value.
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub const fn with_seed(mut self, seed: u64) -> Self {
|
||||
self.seed = Some(seed);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the generator algorithm for maze creation.
|
||||
///
|
||||
/// Different generators may produce different maze patterns and characteristics.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `generator_type` - The maze generation algorithm to use.
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub const fn with_generator(
|
||||
mut self,
|
||||
generator_type: GeneratorType,
|
||||
) -> Self {
|
||||
self.generator_type = generator_type;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the starting position for maze generation.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `pos` - The hexagonal coordinates for the starting position.
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub const fn with_start_position(mut self, pos: Hex) -> Self {
|
||||
self.start_position = Some(pos);
|
||||
self
|
||||
}
|
||||
|
||||
/// Builds the hexagonal maze based on the configured parameters.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`MazeBuilderError::NoRadius`] if no radius is specified.
|
||||
/// Returns [`MazeBuilderError::InvalidStartPosition`] if the start position is outside maze
|
||||
/// bounds.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use hexlab::prelude::*;
|
||||
///
|
||||
/// // Should fail without radius
|
||||
/// let result = MazeBuilder::new().build();
|
||||
/// assert!(result.is_err());
|
||||
///
|
||||
/// // Should succeed with radius
|
||||
/// let result = MazeBuilder::new()
|
||||
/// .with_radius(3)
|
||||
/// .build();
|
||||
/// assert!(result.is_ok());
|
||||
///
|
||||
/// let maze = result.unwrap();
|
||||
/// assert!(!maze.is_empty());
|
||||
/// ```
|
||||
pub fn build(self) -> Result<Maze, MazeBuilderError> {
|
||||
let radius = self.radius.ok_or(MazeBuilderError::NoRadius)?;
|
||||
let mut maze = create_hex_maze(radius);
|
||||
|
||||
if let Some(start_pos) = self.start_position {
|
||||
if maze.get(&start_pos).is_none() {
|
||||
return Err(MazeBuilderError::InvalidStartPosition(start_pos));
|
||||
}
|
||||
}
|
||||
|
||||
if !maze.is_empty() {
|
||||
self.generator_type.generate(
|
||||
&mut maze,
|
||||
self.start_position,
|
||||
self.seed,
|
||||
);
|
||||
}
|
||||
|
||||
Ok(maze)
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
pub fn generate_backtracking(
|
||||
maze: &mut HexMaze,
|
||||
start_pos: Option<Hex>,
|
||||
seed: Option<u64>,
|
||||
) {
|
||||
if maze.is_empty() {
|
||||
return;
|
||||
}
|
||||
let start = start_pos.unwrap_or(Hex::ZERO);
|
||||
let mut visited = HashSet::new();
|
||||
let mut rng: Box<dyn RngCore> = seed.map_or_else(
|
||||
|| Box::new(thread_rng()) as Box<dyn RngCore>,
|
||||
|seed| Box::new(ChaCha8Rng::seed_from_u64(seed)) as Box<dyn RngCore>,
|
||||
);
|
||||
recursive_backtrack(maze, start, &mut visited, &mut rng);
|
||||
}
|
||||
|
||||
fn recursive_backtrack<R: Rng>(
|
||||
maze: &mut HexMaze,
|
||||
current: Hex,
|
||||
visited: &mut HashSet<Hex>,
|
||||
rng: &mut R,
|
||||
) {
|
||||
visited.insert(current);
|
||||
let mut directions = EdgeDirection::ALL_DIRECTIONS;
|
||||
directions.shuffle(rng);
|
||||
for direction in directions {
|
||||
let neighbor = current + direction;
|
||||
if maze.get_tile(&neighbor).is_some() && !visited.contains(&neighbor) {
|
||||
maze.remove_tile_wall(¤t, direction);
|
||||
maze.remove_tile_wall(&neighbor, direction.const_neg());
|
||||
recursive_backtrack(maze, neighbor, visited, rng);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
#[derive(Default)]
|
||||
pub struct MazeBuilder {
|
||||
radius: Option<u32>,
|
||||
seed: Option<u64>,
|
||||
generator_type: GeneratorType,
|
||||
start_position: Option<Hex>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub enum GeneratorType {
|
||||
#[default]
|
||||
RecursiveBacktracking,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct HexMaze(HashMap<Hex, HexTile>);
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "bevy", derive(Reflect, Component))]
|
||||
#[cfg_attr(feature = "bevy", reflect(Component))]
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct HexTile {
|
||||
pub(crate) pos: Hex,
|
||||
pub(crate) walls: Walls,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "bevy", derive(Reflect, Component))]
|
||||
#[cfg_attr(feature = "bevy", reflect(Component))]
|
||||
pub struct Walls(u8);
|
||||
197
assets/code/hexlab/walls.rs
Normal file
197
assets/code/hexlab/walls.rs
Normal file
@@ -0,0 +1,197 @@
|
||||
/// A bit-flag representation of walls in a hexagonal tile.
|
||||
///
|
||||
/// `Walls` uses an efficient bit-flag system to track the presence or absence of walls
|
||||
/// along each edge of a hexagonal tile. Each of the six possible walls is represented
|
||||
/// by a single bit in an 8-bit integer, allowing for fast operations and minimal memory usage.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Creating and manipulating walls:
|
||||
/// ```
|
||||
/// use hexlab::prelude::*;
|
||||
///
|
||||
/// // Create a hexagon with all walls
|
||||
/// let walls = Walls::new();
|
||||
/// assert!(walls.is_enclosed());
|
||||
///
|
||||
/// // Create a hexagon with no walls
|
||||
/// let mut walls = Walls::empty();
|
||||
/// assert!(walls.is_empty());
|
||||
///
|
||||
/// // Add specific walls
|
||||
/// walls.insert(EdgeDirection::FLAT_NORTH);
|
||||
/// walls.insert(EdgeDirection::FLAT_SOUTH);
|
||||
/// assert_eq!(walls.count(), 2);
|
||||
/// ```
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
|
||||
#[cfg_attr(feature = "bevy", derive(Component))]
|
||||
#[cfg_attr(feature = "bevy", reflect(Component))]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Walls(u8);
|
||||
|
||||
impl Walls {
|
||||
/// Insert a wall in the specified direction.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `direction` - The direction in which to insert the wall.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Returns `true` if a wall was present, `false` otherwise.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use hexlab::prelude::*;
|
||||
///
|
||||
/// let mut walls = Walls::empty();
|
||||
/// assert_eq!(walls.count(), 0);
|
||||
///
|
||||
/// assert!(!walls.insert(1));
|
||||
/// assert_eq!(walls.count(), 1);
|
||||
///
|
||||
/// assert!(walls.insert(1));
|
||||
/// assert_eq!(walls.count(), 1);
|
||||
///
|
||||
/// assert!(!walls.insert(EdgeDirection::FLAT_NORTH));
|
||||
/// assert_eq!(walls.count(), 2);
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn insert<T>(&mut self, direction: T) -> bool
|
||||
where
|
||||
T: Into<Self>,
|
||||
{
|
||||
let mask = direction.into().0;
|
||||
let was_present = self.0 & mask != 0;
|
||||
self.0 |= mask;
|
||||
was_present
|
||||
}
|
||||
|
||||
/// Removes a wall in the specified direction.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `direction` - The direction from which to remove the wall.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Returns `true` if a wall was present and removed, `false` otherwise.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use hexlab::prelude::*;
|
||||
///
|
||||
/// let mut walls = Walls::new();
|
||||
///
|
||||
/// assert!(walls.remove(1));
|
||||
/// assert_eq!(walls.count(), 5);
|
||||
///
|
||||
/// assert!(!walls.remove(1));
|
||||
/// assert_eq!(walls.count(), 5);
|
||||
///
|
||||
/// assert!(walls.remove(EdgeDirection::FLAT_NORTH));
|
||||
/// assert_eq!(walls.count(), 4);
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn remove<T>(&mut self, direction: T) -> bool
|
||||
where
|
||||
T: Into<Self>,
|
||||
{
|
||||
let mask = direction.into().0;
|
||||
let was_present = self.0 & mask != 0;
|
||||
self.0 &= !mask;
|
||||
was_present
|
||||
}
|
||||
|
||||
/// Checks if there is a wall in the specified direction.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `other` - The direction to check for a wall.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use hexlab::prelude::*;
|
||||
///
|
||||
/// let mut walls = Walls::empty();
|
||||
/// walls.insert(EdgeDirection::FLAT_NORTH);
|
||||
///
|
||||
/// assert!(walls.contains(EdgeDirection::FLAT_NORTH));
|
||||
/// assert!(!walls.contains(EdgeDirection::FLAT_SOUTH));
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn contains<T>(&self, direction: T) -> bool
|
||||
where
|
||||
T: Into<Self>,
|
||||
{
|
||||
self.0 & direction.into().0 != 0
|
||||
}
|
||||
|
||||
/// Toggles a wall in the specified direction.
|
||||
///
|
||||
/// If a wall exists in the given direction, it will be removed.
|
||||
/// If no wall exists, one will be added.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `direction` - The direction in which to toggle the wall.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The previous state (`true` if a wall was present before toggling, `false` otherwise).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use hexlab::prelude::*;
|
||||
///
|
||||
/// let mut walls = Walls::empty();
|
||||
///
|
||||
/// assert!(!walls.toggle(0));
|
||||
/// assert_eq!(walls.count(), 1);
|
||||
///
|
||||
/// assert!(walls.toggle(0));
|
||||
/// assert_eq!(walls.count(), 0);
|
||||
/// ```
|
||||
pub fn toggle<T>(&mut self, direction: T) -> bool
|
||||
where
|
||||
T: Into<Self> + Copy,
|
||||
{
|
||||
let mask = direction.into().0;
|
||||
let was_present = self.0 & mask != 0;
|
||||
self.0 ^= mask;
|
||||
was_present
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EdgeDirection> for Walls {
|
||||
fn from(value: EdgeDirection) -> Self {
|
||||
Self(1 << value.index())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u8> for Walls {
|
||||
fn from(value: u8) -> Self {
|
||||
Self(1 << value)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromIterator<EdgeDirection> for Walls {
|
||||
fn from_iter<T: IntoIterator<Item = EdgeDirection>>(iter: T) -> Self {
|
||||
let mut walls = 0u8;
|
||||
for direction in iter {
|
||||
walls |= 1 << direction.index();
|
||||
}
|
||||
Self(walls)
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> From<[EdgeDirection; N]> for Walls {
|
||||
fn from(value: [EdgeDirection; N]) -> Self {
|
||||
value.into_iter().collect()
|
||||
}
|
||||
}
|
||||
69
assets/code/maze-ascension/floor.rs
Normal file
69
assets/code/maze-ascension/floor.rs
Normal file
@@ -0,0 +1,69 @@
|
||||
/// Move floor entities to their target Y positions based on movement speed
|
||||
///
|
||||
/// # Behavior
|
||||
/// - Calculates movement distance based on player speed and delta time
|
||||
/// - Moves floors towards their target Y position
|
||||
/// - Removes FloorYTarget component when floor reaches destination
|
||||
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; // snap to final position
|
||||
commands.entity(entity).remove::<FloorYTarget>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle floor transition events by setting up floor movement targets
|
||||
///
|
||||
/// # Behavior
|
||||
/// - Checks if any floors are currently moving
|
||||
/// - Processes floor transition events
|
||||
/// - Sets target Y positions for all maze entities
|
||||
/// - Updates current and next floor designations
|
||||
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;
|
||||
}
|
||||
@@ -1,88 +1,159 @@
|
||||
pub(super) fn setup(
|
||||
/// Spawns a new maze floor in response to a SpawnMaze trigger event
|
||||
pub(super) fn spawn_maze(
|
||||
trigger: Trigger<SpawnMaze>,
|
||||
mut commands: Commands,
|
||||
mut meshes: ResMut<Assets<Mesh>>,
|
||||
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||
config: Res<MazeConfig>,
|
||||
layout: Res<Layout>,
|
||||
maze_query: Query<(Entity, &Floor, &Maze)>,
|
||||
global_config: Res<GlobalMazeConfig>,
|
||||
) {
|
||||
let maze = MazeBuilder::new()
|
||||
.with_radius(config.radius)
|
||||
.with_seed(0)
|
||||
.with_generator(GeneratorType::RecursiveBacktracking)
|
||||
.build()
|
||||
.expect("Something went wrong while creating maze");
|
||||
let SpawnMaze { floor, config } = trigger.event();
|
||||
|
||||
let assets = create_base_assets(&mut meshes, &mut materials, &config);
|
||||
commands
|
||||
if maze_query.iter().any(|(_, f, _)| f.0 == *floor) {
|
||||
warn!("Floor {} already exists, skipping creation", floor);
|
||||
return;
|
||||
}
|
||||
|
||||
let maze = match generate_maze(config) {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
error!("Failed to generate maze for floor {floor}: {:?}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Calculate vertical offset based on floor number
|
||||
let y_offset = match *floor {
|
||||
1 => 0, // Ground/Initial floor (floor 1) is at y=0
|
||||
_ => FLOOR_Y_OFFSET, // Other floors are offset vertically
|
||||
} as f32;
|
||||
|
||||
let entity = commands
|
||||
.spawn((
|
||||
Name::new("Floor"),
|
||||
SpatialBundle {
|
||||
transform: Transform::from_translation(Vec3::ZERO),
|
||||
..default()
|
||||
},
|
||||
Name::new(format!("Floor {}", floor)),
|
||||
HexMaze,
|
||||
maze.clone(),
|
||||
Floor(*floor),
|
||||
config.clone(),
|
||||
Transform::from_translation(Vec3::ZERO.with_y(y_offset)),
|
||||
Visibility::Visible,
|
||||
))
|
||||
.with_children(|parent| {
|
||||
for tile in maze.values() {
|
||||
spawn_single_hex_tile(
|
||||
parent,
|
||||
&assets,
|
||||
tile,
|
||||
&layout.0,
|
||||
config.height,
|
||||
)
|
||||
}
|
||||
});
|
||||
.insert_if(CurrentFloor, || *floor == 1) // Only floor 1 gets CurrentFloor
|
||||
.insert_if(NextFloor, || *floor != 1) // All other floors get NextFloor
|
||||
.id();
|
||||
|
||||
let assets = MazeAssets::new(&mut meshes, &mut materials, &global_config);
|
||||
spawn_maze_tiles(
|
||||
&mut commands,
|
||||
entity,
|
||||
&maze,
|
||||
&assets,
|
||||
config,
|
||||
&global_config,
|
||||
);
|
||||
}
|
||||
|
||||
fn spawn_single_hex_tile(
|
||||
/// Spawns all tiles for a maze as children of the parent maze entity
|
||||
pub fn spawn_maze_tiles(
|
||||
commands: &mut Commands,
|
||||
parent_entity: Entity,
|
||||
maze: &Maze,
|
||||
assets: &MazeAssets,
|
||||
maze_config: &MazeConfig,
|
||||
global_config: &GlobalMazeConfig,
|
||||
) {
|
||||
commands.entity(parent_entity).with_children(|parent| {
|
||||
for tile in maze.values() {
|
||||
spawn_single_hex_tile(
|
||||
parent,
|
||||
assets,
|
||||
tile,
|
||||
maze_config,
|
||||
global_config,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Spawns a single hexagonal tile with appropriate transforms and materials
|
||||
pub(super) fn spawn_single_hex_tile(
|
||||
parent: &mut ChildBuilder,
|
||||
assets: &MazeAssets,
|
||||
tile: &HexTile,
|
||||
layout: &HexLayout,
|
||||
hex_height: f32,
|
||||
maze_config: &MazeConfig,
|
||||
global_config: &GlobalMazeConfig,
|
||||
) {
|
||||
dbg!(tile);
|
||||
let world_pos = tile.to_vec3(layout);
|
||||
let rotation = match layout.orientation {
|
||||
let world_pos = tile.to_vec3(&maze_config.layout);
|
||||
let rotation = match maze_config.layout.orientation {
|
||||
// Pointy hexagons don't need additional rotation (0 degrees)
|
||||
HexOrientation::Pointy => Quat::from_rotation_y(0.0),
|
||||
HexOrientation::Flat => Quat::from_rotation_y(FRAC_PI_6), // 30 degrees rotation
|
||||
// Flat-top hexagons need 30 degrees (pi/6) rotation around Y axis
|
||||
HexOrientation::Flat => Quat::from_rotation_y(FRAC_PI_6),
|
||||
};
|
||||
|
||||
// Select material based on tile position: start, end, or default
|
||||
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.to_string())),
|
||||
PbrBundle {
|
||||
mesh: assets.hex_mesh.clone(),
|
||||
material: assets.hex_material.clone(),
|
||||
transform: Transform::from_translation(world_pos)
|
||||
.with_rotation(rotation),
|
||||
..default()
|
||||
},
|
||||
Name::new(format!("Hex {}", tile)),
|
||||
Tile,
|
||||
Mesh3d(assets.hex_mesh.clone()),
|
||||
MeshMaterial3d(material),
|
||||
Transform::from_translation(world_pos).with_rotation(rotation),
|
||||
))
|
||||
.with_children(|parent| {
|
||||
spawn_walls(parent, assets, hex_height / 2., &tile.walls())
|
||||
spawn_walls(parent, assets, tile.walls(), global_config)
|
||||
});
|
||||
}
|
||||
|
||||
/// Spawns walls around a hexagonal tile based on the walls configuration
|
||||
fn spawn_walls(
|
||||
parent: &mut ChildBuilder,
|
||||
assets: &MazeAssets,
|
||||
y_offset: f32,
|
||||
walls: &Walls,
|
||||
global_config: &GlobalMazeConfig,
|
||||
) {
|
||||
// Base rotation for wall alignment (90 degrees counter-clockwise)
|
||||
let z_rotation = Quat::from_rotation_z(-FRAC_PI_2);
|
||||
let y_offset = global_config.height / 2.;
|
||||
|
||||
for i in 0..6 {
|
||||
if !walls.contains(i) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Calculate the angle for this wall
|
||||
// FRAC_PI_3 = 60 deg
|
||||
// Negative because going clockwise
|
||||
// i * 60 produces: 0, 60, 120, 180, 240, 300
|
||||
let wall_angle = -FRAC_PI_3 * i as f32;
|
||||
|
||||
let x_offset = (HEX_SIZE - WALL_SIZE) * f32::cos(wall_angle);
|
||||
let z_offset = (HEX_SIZE - WALL_SIZE) * f32::sin(wall_angle);
|
||||
// cos(angle) gives x coordinate on unit circle
|
||||
// sin(angle) gives z coordinate on unit circle
|
||||
// Multiply by wall_offset to get actual distance from center
|
||||
let x_offset = global_config.wall_offset() * f32::cos(wall_angle);
|
||||
let z_offset = global_config.wall_offset() * f32::sin(wall_angle);
|
||||
|
||||
// x: distance along x-axis from center
|
||||
// y: vertical offset from center
|
||||
// z: distance along z-axis from center
|
||||
let pos = Vec3::new(x_offset, y_offset, z_offset);
|
||||
|
||||
// 1. Rotate around x-axis to align wall with angle
|
||||
// 2. Add FRAC_PI_2 (90) to make wall perpendicular to angle
|
||||
let x_rotation = Quat::from_rotation_x(wall_angle + FRAC_PI_2);
|
||||
let final_rotation = z_rotation * x_rotation;
|
||||
|
||||
@@ -90,52 +161,18 @@ fn spawn_walls(
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawns a single wall segment with the specified rotation and position
|
||||
fn spawn_single_wall(
|
||||
parent: &mut ChildBuilder,
|
||||
asstets: &MazeAssets,
|
||||
assets: &MazeAssets,
|
||||
rotation: Quat,
|
||||
offset: Vec3,
|
||||
) {
|
||||
parent.spawn((
|
||||
Name::new("Wall"),
|
||||
PbrBundle {
|
||||
mesh: asstets.wall_mesh.clone(),
|
||||
material: asstets.wall_material.clone(),
|
||||
transform: Transform::from_translation(offset)
|
||||
.with_rotation(rotation),
|
||||
..default()
|
||||
},
|
||||
Wall,
|
||||
Mesh3d(assets.wall_mesh.clone()),
|
||||
MeshMaterial3d(assets.wall_material.clone()),
|
||||
Transform::from_translation(offset).with_rotation(rotation),
|
||||
));
|
||||
}
|
||||
|
||||
fn create_base_assets(
|
||||
meshes: &mut ResMut<Assets<Mesh>>,
|
||||
materials: &mut ResMut<Assets<StandardMaterial>>,
|
||||
config: &Res<MazeConfig>,
|
||||
) -> MazeAssets {
|
||||
MazeAssets {
|
||||
hex_mesh: meshes.add(generate_hex_mesh(HEX_SIZE, config.height)),
|
||||
wall_mesh: meshes.add(generate_square_mesh(HEX_SIZE)),
|
||||
hex_material: materials.add(white_material()),
|
||||
wall_material: materials.add(Color::BLACK),
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_hex_mesh(radius: f32, depth: f32) -> Mesh {
|
||||
let hexagon = RegularPolygon {
|
||||
sides: 6,
|
||||
circumcircle: Circle::new(radius),
|
||||
};
|
||||
let prism_shape = Extrusion::new(hexagon, depth);
|
||||
let rotation = Quat::from_rotation_x(FRAC_PI_2);
|
||||
|
||||
Mesh::from(prism_shape).rotated_by(rotation)
|
||||
}
|
||||
|
||||
fn generate_square_mesh(depth: f32) -> Mesh {
|
||||
let square = Rectangle::new(WALL_SIZE, WALL_SIZE);
|
||||
let rectangular_prism = Extrusion::new(square, depth);
|
||||
let rotation = Quat::from_rotation_x(FRAC_PI_2);
|
||||
|
||||
Mesh::from(rectangular_prism).rotated_by(rotation)
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 428 KiB |
Reference in New Issue
Block a user