feat: add more code examples

This commit is contained in:
Kristofers Solo 2025-01-03 00:24:47 +02:00
parent c4bbd7c88c
commit 84b95fac6a
14 changed files with 654 additions and 245 deletions

View 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)
}
}

View File

@ -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(&current, direction);
maze.remove_tile_wall(&neighbor, direction.const_neg());
recursive_backtrack(maze, neighbor, visited, rng);
}
}
}

View File

@ -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
View 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()
}
}

View 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;
}

View File

@ -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 commands: Commands,
mut meshes: ResMut<Assets<Mesh>>, mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>, mut materials: ResMut<Assets<StandardMaterial>>,
config: Res<MazeConfig>, maze_query: Query<(Entity, &Floor, &Maze)>,
layout: Res<Layout>, global_config: Res<GlobalMazeConfig>,
) { ) {
let maze = MazeBuilder::new() let SpawnMaze { floor, config } = trigger.event();
.with_radius(config.radius)
.with_seed(0)
.with_generator(GeneratorType::RecursiveBacktracking)
.build()
.expect("Something went wrong while creating maze");
let assets = create_base_assets(&mut meshes, &mut materials, &config); if maze_query.iter().any(|(_, f, _)| f.0 == *floor) {
commands 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(( .spawn((
Name::new("Floor"), Name::new(format!("Floor {}", floor)),
SpatialBundle { HexMaze,
transform: Transform::from_translation(Vec3::ZERO), maze.clone(),
..default() Floor(*floor),
}, config.clone(),
Transform::from_translation(Vec3::ZERO.with_y(y_offset)),
Visibility::Visible,
)) ))
.with_children(|parent| { .insert_if(CurrentFloor, || *floor == 1) // Only floor 1 gets CurrentFloor
for tile in maze.values() { .insert_if(NextFloor, || *floor != 1) // All other floors get NextFloor
spawn_single_hex_tile( .id();
parent,
&assets, let assets = MazeAssets::new(&mut meshes, &mut materials, &global_config);
tile, spawn_maze_tiles(
&layout.0, &mut commands,
config.height, 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, parent: &mut ChildBuilder,
assets: &MazeAssets, assets: &MazeAssets,
tile: &HexTile, tile: &HexTile,
layout: &HexLayout, maze_config: &MazeConfig,
hex_height: f32, global_config: &GlobalMazeConfig,
) { ) {
dbg!(tile); let world_pos = tile.to_vec3(&maze_config.layout);
let world_pos = tile.to_vec3(layout); let rotation = match maze_config.layout.orientation {
let rotation = match layout.orientation { // Pointy hexagons don't need additional rotation (0 degrees)
HexOrientation::Pointy => Quat::from_rotation_y(0.0), 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 parent
.spawn(( .spawn((
Name::new(format!("Hex {}", tile.to_string())), Name::new(format!("Hex {}", tile)),
PbrBundle { Tile,
mesh: assets.hex_mesh.clone(), Mesh3d(assets.hex_mesh.clone()),
material: assets.hex_material.clone(), MeshMaterial3d(material),
transform: Transform::from_translation(world_pos) Transform::from_translation(world_pos).with_rotation(rotation),
.with_rotation(rotation),
..default()
},
)) ))
.with_children(|parent| { .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( fn spawn_walls(
parent: &mut ChildBuilder, parent: &mut ChildBuilder,
assets: &MazeAssets, assets: &MazeAssets,
y_offset: f32,
walls: &Walls, 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 z_rotation = Quat::from_rotation_z(-FRAC_PI_2);
let y_offset = global_config.height / 2.;
for i in 0..6 { for i in 0..6 {
if !walls.contains(i) { if !walls.contains(i) {
continue; 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 wall_angle = -FRAC_PI_3 * i as f32;
let x_offset = (HEX_SIZE - WALL_SIZE) * f32::cos(wall_angle); // cos(angle) gives x coordinate on unit circle
let z_offset = (HEX_SIZE - WALL_SIZE) * f32::sin(wall_angle); // 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); 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 x_rotation = Quat::from_rotation_x(wall_angle + FRAC_PI_2);
let final_rotation = z_rotation * x_rotation; 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( fn spawn_single_wall(
parent: &mut ChildBuilder, parent: &mut ChildBuilder,
asstets: &MazeAssets, assets: &MazeAssets,
rotation: Quat, rotation: Quat,
offset: Vec3, offset: Vec3,
) { ) {
parent.spawn(( parent.spawn((
Name::new("Wall"), Name::new("Wall"),
PbrBundle { Wall,
mesh: asstets.wall_mesh.clone(), Mesh3d(assets.wall_mesh.clone()),
material: asstets.wall_material.clone(), MeshMaterial3d(assets.wall_material.clone()),
transform: Transform::from_translation(offset) Transform::from_translation(offset).with_rotation(rotation),
.with_rotation(rotation),
..default()
},
)); ));
} }
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

View File

@ -7,7 +7,7 @@ typst:
- Haug - Haug
- Martin - Martin
- Typst Projekta Izstrādātāji - Typst Projekta Izstrādātāji
url: {value: "https://typst.app/", date: 2024-12-20} url: "https://typst.app/"
hex-grid: hex-grid:
type: Web type: Web
title: Hexagonal Grids title: Hexagonal Grids
@ -44,11 +44,6 @@ maze-generation:
title: Maze Generation title: Maze Generation
author: author:
url: https://rosettacode.org/wiki/Maze_generation url: https://rosettacode.org/wiki/Maze_generation
bevy-quickstart:
type: Web
title: Bevy New 2D
author:
url: https://github.com/TheBevyFlock/bevy_new_2d
sem-ver: sem-ver:
type: Web type: Web
title: Semantiskā versiju veidošana title: Semantiskā versiju veidošana
@ -84,7 +79,7 @@ the-rust-performance-book:
url: https://nnethercote.github.io/perf-book url: https://nnethercote.github.io/perf-book
cargo-tarpaulin: cargo-tarpaulin:
type: Web type: Web
title: Tarpaulin title: Tarpaulin rīks
author: xd009642 author: xd009642
url: {value: "https://crates.io/crates/cargo-tarpaulin", date: 2024-12-18} url: {value: "https://crates.io/crates/cargo-tarpaulin", date: 2024-12-18}
ecs: ecs:
@ -94,7 +89,7 @@ ecs:
url: {value: "https://en.wikipedia.org/wiki/Entity_component_system", date: 2024-09-12} url: {value: "https://en.wikipedia.org/wiki/Entity_component_system", date: 2024-09-12}
bevy-ecs: bevy-ecs:
type: Web type: Web
title: Bevy ECS title: Ievads Bevy ECS
author: Bevy Projekta Izstādātāji author: Bevy Projekta Izstādātāji
url: {value: "https://bevyengine.org/learn/quick-start/getting-started/ecs/", date: 2024-09-12} url: {value: "https://bevyengine.org/learn/quick-start/getting-started/ecs/", date: 2024-09-12}
SRP: SRP:
@ -117,12 +112,12 @@ begginer-patterns:
url: "https://pressbooks.lib.jmu.edu/programmingpatterns/" url: "https://pressbooks.lib.jmu.edu/programmingpatterns/"
clippy: clippy:
type: Web type: Web
title: Clippy title: Clippy dokumentācija
author: Rust Projekta Izstādātāji author: Rust Projekta Izstādātāji
url: https://doc.rust-lang.org/stable/clippy/ url: https://doc.rust-lang.org/stable/clippy/
cargo-doc: cargo-doc:
type: Web type: Web
title: cargo-doc title: cargo-doc dokumentācija
author: Rust Projekta Izstādātāji author: Rust Projekta Izstādātāji
url: https://doc.rust-lang.org/stable/cargo/ url: https://doc.rust-lang.org/stable/cargo/
rust-style: rust-style:
@ -137,12 +132,12 @@ rust-lang-doc:
url: https://doc.rust-lang.org/stable/ url: https://doc.rust-lang.org/stable/
rustfmt: rustfmt:
type: Web type: Web
title: Rustfmt title: Rustfmt dokumentācija
author: Rust Projekta Izstādātāji author: Rust Projekta Izstādātāji
url: https://github.com/rust-lang/rustfmt url: https://github.com/rust-lang/rustfmt
gh-release: gh-release:
type: Web type: Web
title: About Releases title: About Releases dokumentācija
author: GitHub komanda author: GitHub komanda
url: https://docs.github.com/en/repositories/releasing-projects-on-github/about-releases url: https://docs.github.com/en/repositories/releasing-projects-on-github/about-releases
gh-actions: gh-actions:
@ -152,13 +147,13 @@ gh-actions:
url: https://docs.github.com/en/actions url: https://docs.github.com/en/actions
tokei: tokei:
type: Web type: Web
title: tokei title: Tokei rīks
author: XAMPPRocky author: XAMPPRocky
url: https://crates.io/crates/tokei url: https://crates.io/crates/tokei
QSM: QSM:
type: Web type: Web
title: Software Project Performance Benchmark Tables title: Software Project Performance Benchmark Tables
author: QSM, Inc. author: QSM
url: https://www.qsm.com/resources/qsm-benchmark-tables url: https://www.qsm.com/resources/qsm-benchmark-tables
bevy-0.15: bevy-0.15:
type: Web type: Web

View File

@ -1,18 +1,20 @@
#import "utils.typ": * #import "utils.typ": *
"hexlab" bibliotēkas datu struktūras un svarīgās funkcijas. #codeblock(
[Labirinta būvētāja implementācijas piemērs],
"assets/code/hexlab/builder.rs",
)
#codeblock(
[Labirinta sienu reprezentācijas piemērs],
"assets/code/hexlab/walls.rs",
)
#codeblock(
[Labirinta stāvu implementācijas piemērs],
"assets/code/maze-ascension/floor.rs",
)
#context { #codeblock(
set par.line(numbering: "1") [Labirinta ģenerācijas implementācijas piemērs],
codeblock("./assets/code/hexlab/structs.rs", "rust") "assets/code/maze-ascension/maze_generation.rs",
codeblock("./assets/code/hexlab/generation.rs", "rust") )
}
Spēles datu struktūras un svarīgās funkcijas.
#context {
set par.line(numbering: "1")
codeblock("./assets/code/maze-ascension/components.rs", "rust")
codeblock("./assets/code/maze-ascension/maze_generation.rs", "rust")
}

18
doc.typ
View File

@ -9,11 +9,13 @@
#heading(numbering: none, outlined: false, "Dokumentārā lapa") #heading(numbering: none, outlined: false, "Dokumentārā lapa")
Kvalifikācijas darbs "*Spēles izstrāde, izmantojot Bevy spēļu dzinēju*" ir Kvalifikācijas darbs "*Spēles izstrāde, izmantojot Bevy spēļu dzinēju*" ir
izstrādāts Latvijas Universitātes eksakto zinātņu un tehnoloģiju fakultātē. izstrādāts Latvijas Universitātes Eksakto zinātņu un tehnoloģiju fakultātē,
Datorikas nodaļā.
#v(vspace / 3)
Ar savu parakstu apliecinu, ka darbs izstrādāts patstāvīgi, izmantoti tikai tajā Ar savu parakstu apliecinu, ka darbs izstrādāts patstāvīgi, izmantoti tikai tajā
norādītie informācijas avoti un iesniegtā darba elektroniskā kopija atbilst norādītie informācijas avoti un iesniegtā darba elektroniskā kopija atbilst
izdrukai. izdrukai un/vai recenzentam uzrādītajai darba versijai.
#context { #context {
@ -23,8 +25,8 @@ izdrukai.
hanging-indent: 1cm, hanging-indent: 1cm,
) )
v(vspace) v(vspace / 2)
[Darba autors: *Kristiāns Francis Cagulis, kc22015 ~~\_\_.01.2025.*] [Autors: *Kristiāns Francis Cagulis, kc22015 ~~\_\_.01.2025.*]
v(vspace) v(vspace)
[Rekomendēju darbu aizstāvēšanai\ [Rekomendēju darbu aizstāvēšanai\
@ -36,14 +38,8 @@ izdrukai.
v(vspace) v(vspace)
[Darbs iesniegs *\_\_.01.2025.*\ [Darbs iesniegs *\_\_.01.2025.*\
Kvalifikācijas darbu pārbaudījumu komisijas sekretārs(-e): #long-underline Kvalifikācijas darbu pārbaudījumu komisijas sekretārs (elektronisks paraksts)
] ]
v(vspace) v(vspace)
[Darbs aizstāvēts kvalifikācijas darbu pārbaudījuma komisijas sēdē\
\_\_.01.2025. prot. Nr. #long-underline
]
v(vspace / 2)
[Komisijas sekretārs(-e): #long-underline]
v(vspace)
} }

View File

@ -39,9 +39,10 @@
lang: "lv", lang: "lv",
region: "lv", region: "lv",
) )
show raw: set text(font: "JetBrainsMono NF") show raw: set text(
font: "Fira Code",
show raw.where(lang: "pintora"): it => pintorita.render(it.text) features: (calt: 0),
)
show math.equation: set text(weight: 400) show math.equation: set text(weight: 400)
@ -172,8 +173,7 @@
) )
// WARNING: remove before sending // WARNING: remove before sending
outline(title: "TODOs", target: figure.where(kind: "todo")) // outline(title: "TODOs", target: figure.where(kind: "todo"))
/* --- Figure/Table config start --- */ /* --- Figure/Table config start --- */
show heading: i-figured.reset-counters show heading: i-figured.reset-counters
show figure: i-figured.show-figure.with(numbering: "1.1.") show figure: i-figured.show-figure.with(numbering: "1.1.")
@ -183,6 +183,8 @@
show figure.where(kind: "i-figured-table"): set block(breakable: true) show figure.where(kind: "i-figured-table"): set block(breakable: true)
show figure.where(kind: "i-figured-table"): set figure.caption(position: top) show figure.where(kind: "i-figured-table"): set figure.caption(position: top)
show figure.where(kind: "attachment"): set figure.caption(position: top) show figure.where(kind: "attachment"): set figure.caption(position: top)
show figure.where(kind: raw): set figure.caption(position: top)
show figure: set par(justify: false) // disable justify for figures (tables) show figure: set par(justify: false) // disable justify for figures (tables)
show figure.where(kind: table): set par(leading: 1em) show figure.where(kind: table): set par(leading: 1em)
@ -208,7 +210,10 @@
), ),
) )
} }
if it.kind == "i-figured-\"attachment\"" { if it.kind in (
"i-figured-raw",
"i-figured-\"attachment\"",
) {
return align( return align(
end, end,
it.counter.display() + ". pielikums. " + text(it.body), it.counter.display() + ". pielikums. " + text(it.body),
@ -283,7 +288,6 @@
) )
} }
// Default case for non-figure elements // Default case for non-figure elements
it it
} }

View File

@ -20,8 +20,6 @@
) )
#set heading(numbering: none) #set heading(numbering: none)
= Apzīmējumu saraksts = Apzīmējumu saraksts
/ Šūna:
/ Audio: Skaņas komponentes, kas ietver gan skaņas efektus, gan fona mūziku; / Audio: Skaņas komponentes, kas ietver gan skaņas efektus, gan fona mūziku;
/ CI/CD: nepārtraukta integrācija un nepārtraukta izvietošana; / CI/CD: nepārtraukta integrācija un nepārtraukta izvietošana;
/ DPD: datu plūsmas diagramma; / DPD: datu plūsmas diagramma;
@ -38,7 +36,8 @@
/ Režģis: Strukturēts šūnu izkārtojums, kas veido spēles pasaules pamata struktūru; / Režģis: Strukturēts šūnu izkārtojums, kas veido spēles pasaules pamata struktūru;
/ Spēlētājs: lietotāja ieraksts vienas virtuālās istabas kontekstā; / Spēlētājs: lietotāja ieraksts vienas virtuālās istabas kontekstā;
/ Sēkla: Skaitliska vērtība, ko izmanto nejaušo skaitļu ģeneratora inicializēšanai; / Sēkla: Skaitliska vērtība, ko izmanto nejaušo skaitļu ģeneratora inicializēšanai;
/ Šūna: Sešstūraina režģa viena pozīcija, kas definē telpu, kuru var aizņemt viena plāksne; / Šūna: Sešstūraina režģa viena pozīcija, kas definē telpu, kuru var aizņemt viena plāksne.
/ WASM: WebAssembly -- zema līmeņa assemblera tipa kods, kas var darboties modernos tīmekļa pārlūkos.
= Ievads = Ievads
== Nolūks == Nolūks
@ -51,13 +50,6 @@ procedurālu labirintu ģenerēšanu, spēlētāju navigācijas sistēmu, papild
integrāciju un vertikālās progresijas mehāniku, vienlaikus ievērojot minimālisma integrāciju un vertikālās progresijas mehāniku, vienlaikus ievērojot minimālisma
dizaina filozofiju. dizaina filozofiju.
// Spēles pamatā ir sešstūra formas šūnas, kas, savukārt, veido sešstūra
// formas labirintus, kuri rada atšķirīgu vizuālo un navigācijas izaicinājumu.
// Spēlētāju uzdevums ir pārvietoties pa šiem labirintiem, lai sasniegtu katra
// līmeņa beigas. Spēlētājiem progresējot, tie sastopas ar arvien sarežģītākiem
// labirintiem, kuros nepieciešama stratēģiska domāšana, izpēte un papildspēju
// izmantošana.
Spēles pamatā ir procedurāli ģenerēti sešstūra labirinti, kas katrā spēlē rada Spēles pamatā ir procedurāli ģenerēti sešstūra labirinti, kas katrā spēlē rada
unikālu vizuālo un navigācijas izaicinājumu. Procedurālās ģenerēšanas sistēma unikālu vizuālo un navigācijas izaicinājumu. Procedurālās ģenerēšanas sistēma
nodrošina, ka: nodrošina, ka:
@ -96,8 +88,8 @@ tehnisko iespējamību.
== Saistība ar citiem dokumentiem == Saistība ar citiem dokumentiem
PPS ir izstrādāta, ievērojot LVS 68:1996 "Programmatūras prasību specifikācijas PPS ir izstrādāta, ievērojot LVS 68:1996 "Programmatūras prasību specifikācijas
ceļvedis" un LVS 72:1996 "Ieteicamā prakse programmatūras projektējuma ceļvedis" @lvs_68 un LVS 72:1996 "Ieteicamā prakse programmatūras projektējuma
aprakstīšanai" standarta prasības @lvs_68 @lvs_72. aprakstīšanai" standarta prasības @lvs_72.
== Pārskats == Pārskats
Šis dokuments sniedz detalizētu programmatūras prasību specifikāciju spēlei Šis dokuments sniedz detalizētu programmatūras prasību specifikāciju spēlei
@ -248,14 +240,18 @@ Ar lietotājiem saistītās datu plūsmas ir attēlotas sistēmas nultā līmeņ
+ Programmēšanas valodas un Bevy spēles dzinēja tehniskie ierobežojumi; + Programmēšanas valodas un Bevy spēles dzinēja tehniskie ierobežojumi;
+ Responsivitāte; + Responsivitāte;
+ Starpplatformu savietojamība: Linux, macOS, Windows un WebAssembly. + Starpplatformu savietojamība: Linux, macOS, Windows un WebAssembly.
// + Izplatīšanas un izvietošanas ierobežojumi:
// + CI/CD darbplūsma. #indent-par[
Dokumentācijas izstrādei ir izmantots Typst rīks, kas nodrošina efektīvu darbu
ar tehnisko dokumentāciju, ieskaitot matemātiskas formulas, diagrammas un koda
fragmentus @typst.
]
== Pieņēmumi un atkarības == Pieņēmumi un atkarības
- Tehniskie pieņēmumi: - Tehniskie pieņēmumi:
- Spēlētāja ierīcei jāatbilst minimālajām aparatūras prasībām, lai varētu - Spēlētāja ierīcei jāatbilst minimālajām aparatūras prasībām, lai varētu
palaist uz Bevy spēles dzinēja balstītas spēles. palaist uz Bevy spēles dzinēja balstītas spēles.
- ierīcei jāatbalsta WebGL2,#footnote("https://registry.khronos.org/webgl/specs/latest/2.0/") - ierīcei jāatbalsta WebGL2 #footnote("https://registry.khronos.org/webgl/specs/latest/2.0/"),
lai nodrošinātu pareizu atveidošanu @webgl2. lai nodrošinātu pareizu atveidošanu @webgl2.
- tīmekļa spēļu spēlēšanai (WebAssembly versija) pārlūkprogrammai jābūt mūsdienīgai un saderīgai ar WebAssembly. - tīmekļa spēļu spēlēšanai (WebAssembly versija) pārlūkprogrammai jābūt mūsdienīgai un saderīgai ar WebAssembly.
- ekrāna izšķirtspējai jābūt vismaz 800x600 pikseļu, lai spēle būtu optimāla. - ekrāna izšķirtspējai jābūt vismaz 800x600 pikseļu, lai spēle būtu optimāla.
@ -1700,9 +1696,7 @@ projekta lietojuma gadījumam sekojošu iemeslu dēļ:
== Saskarņu projektējums == Saskarņu projektējums
Spēles saskarņu projektējums ietver divus galvenos skatus (sk. @fig:ui-flow) -- Spēles saskarņu projektējums ietver divus galvenos skatus (sk. @fig:ui-flow) --
galveno izvēlni, spēles saskarni -- un izstrādes rīkus. galveno izvēlni un spēles saskarni -- un izstrādes rīkus.
Katra saskarne ir veidota, ņemot vērā tās specifisko lietojuma gadījumu un
lietotāju vajadzības.
#figure( #figure(
caption: "Ekrānskatu plūsmu diagramma", caption: "Ekrānskatu plūsmu diagramma",
@ -1714,7 +1708,7 @@ lietotāju vajadzības.
stroke: 1pt, stroke: 1pt,
"<|-|>", "<|-|>",
) )
action-node((1, 0), [Galvenais ekrāns], inset: 2em) action-node((1, 0), [Spēles ekrāns], inset: 2em)
}, },
), ),
) <ui-flow> ) <ui-flow>
@ -1725,8 +1719,6 @@ Galvenā izvēlne ir pirmais skats, ar ko saskaras lietotājs, uzsākot spēli (
@fig:main-menu). @fig:main-menu).
Tā sastāv no spēles nosaukuma, "Play" -- sākt spēli pogas un "Quit" -- iziet Tā sastāv no spēles nosaukuma, "Play" -- sākt spēli pogas un "Quit" -- iziet
pogas. pogas.
Izvēlnes dizains ir minimālistisks un intuitīvs, izmantojot kontrastējošas
krāsas un skaidru vizuālo hierarhiju.
#figure( #figure(
caption: "Galvenās izvēlnes skats", caption: "Galvenās izvēlnes skats",
@ -1957,8 +1949,8 @@ no drošina piemēra koda pareizību, moduļu testi pārbauda iekšējo
funkcionalitāti, savukārt testu mapē esošie vienībtesti un integrācijas testi funkcionalitāti, savukārt testu mapē esošie vienībtesti un integrācijas testi
pārbauda sarežģītākus gadījumus. pārbauda sarežģītākus gadījumus.
Automatizēto testu izpildes rezultātu kopsavilkums ir redzams Automatizēto testu izpildes rezultātu kopsavilkums ir redzams
@fig:tests-hexlab[attēlā], savukārt detalizēts testu izpildes pārskats ir
pieejams @tests-hexlab-full[pielikumā]. pieejams @tests-hexlab-full[pielikumā].
@fig:tests-hexlab[attēlā], savukārt detalizēts testu izpildes pārskats ir
Izmantojot "cargo-tarpaulin", testu pārklājums ir $81.69%$ (116 no 142 Izmantojot "cargo-tarpaulin", testu pārklājums ir $81.69%$ (116 no 142
iekļautajām rindiņām) (sk. @tarpaulin-hexlab[pielikumu]), tomēr šis rādītājs iekļautajām rindiņām) (sk. @tarpaulin-hexlab[pielikumu]), tomēr šis rādītājs
@ -1971,15 +1963,14 @@ funkcijām un citi tehniski ierobežojumi @cargo-tarpaulin.
image("assets/images/tests/hexlab-minimized.png"), image("assets/images/tests/hexlab-minimized.png"),
)<tests-hexlab> )<tests-hexlab>
Arī spēles kods saglabā stabilu testēšanas stratēģiju. #indent-par[
Dokumentācijas testi tiek rakstīti tieši koda dokumentācijā, kalpojot diviem Arī spēles kods saglabā stabilu testēšanas stratēģiju.
mērķiem -- tie pārbauda koda pareizību un vienlaikus sniedz skaidrus lietošanas Moduļu testi ir stratēģiski izvietoti līdzās implementācijas kodam tajā pašā
piemērus turpmākai uzturēšanai. failā, nodrošinot, ka katras komponentes funkcionalitāte tiek pārbaudīta
Moduļu testi ir stratēģiski izvietoti līdzās implementācijas kodam tajā pašā izolēti.
failā, nodrošinot, ka katras komponentes funkcionalitāte tiek pārbaudīta Šie testi attiecas uz tādām svarīgām spēles sistēmām kā spēlētāju kustība,
izolēti. sadursmju noteikšana, spēles stāvokļa pārvaldība u.c.
Šie testi attiecas uz tādām svarīgām spēles sistēmām kā spēlētāju kustība, ]
sadursmju noteikšana, spēles stāvokļa pārvaldība u.c.
Visi testi tiek automātiski izpildīti kā nepārtrauktas integrācijas procesa Visi testi tiek automātiski izpildīti kā nepārtrauktas integrācijas procesa
daļa, nodrošinot tūlītēju atgriezenisko saiti par sistēmas stabilitāti un daļa, nodrošinot tūlītēju atgriezenisko saiti par sistēmas stabilitāti un
@ -2014,11 +2005,10 @@ dokumentētas#footnote[https://docs.rs/hexlab/latest/hexlab/]<hexlab-docs>.
Šajā dokumentācijā ir ietverti detalizēti apraksti un lietošanas piemēri, kas ne Šajā dokumentācijā ir ietverti detalizēti apraksti un lietošanas piemēri, kas ne
tikai palīdz saprast kodu, bet programmatūras prasības specifikācija ir tikai palīdz saprast kodu, bet programmatūras prasības specifikācija ir
izstrādāta, ievērojot LVS 68:1996 standarta "Programmatūras prasību izstrādāta, ievērojot LVS 68:1996 standarta "Programmatūras prasību
specifikācijas ceļvedis" un LVS 72:1996 standarta "Ieteicamā prakse specifikācijas ceļvedis" @lvs_68 un LVS 72:1996 standarta "Ieteicamā prakse
programmatūras projektējuma aprakstīšanai" standarta prasības @lvs_68 @lvs_72. programmatūras projektējuma aprakstīšanai" standarta prasības @lvs_72.
// Programmatūras projektējuma aprakstā iekļautās Programmatūras projektējuma aprakstā iekļautās aktivitāšu diagrammas ir veidotas
// aktivitāšu diagrammas ir izstrādātas, ievērojot UML 2.5 versijas atbilstoši UML (Unified Modeling Language) 2.5 specifikācijai @omg-uml.
// specifikāciju@omg-uml.
== Konfigurācijas pārvaldība == Konfigurācijas pārvaldība
@ -2029,7 +2019,7 @@ Rīku konfigurācija ir definēta vairākos failos:
laidiena komandas dažādām vidēm: laidiena komandas dažādām vidēm:
- atkļūdošanas kompilācijas ar iespējotu pilnu atpakaļsekošanu; - atkļūdošanas kompilācijas ar iespējotu pilnu atpakaļsekošanu;
- laidiena kompilācijas ar iespējotu optimizāciju. - laidiena kompilācijas ar iespējotu optimizāciju.
- "GitHub Actions"@gh-actions darbplūsmas, kas apstrādā: - "GitHub Actions" darbplūsmas, kas apstrādā @gh-actions:
- koda kvalitātes pārbaudes (vienībtesti, statiskie testi, formatēšana, - koda kvalitātes pārbaudes (vienībtesti, statiskie testi, formatēšana,
dokumentācijas izveide). dokumentācijas izveide).
- kompilācijas un izvietotošanas darbplūsma, kas: - kompilācijas un izvietotošanas darbplūsma, kas:
@ -2051,10 +2041,11 @@ kas parādija, ka "Maze Ascension" projekts satur $1927$ koda rindiņas, bet
saistītā "hexlab" bibliotēka -- $979$ rindiņas, kopā veidojot $2906$ loģiskās koda saistītā "hexlab" bibliotēka -- $979$ rindiņas, kopā veidojot $2906$ loģiskās koda
rindiņas, neiekļaujot tukšās rindiņas un komentārus (sk. @tokei-maze-ascension[] rindiņas, neiekļaujot tukšās rindiņas un komentārus (sk. @tokei-maze-ascension[]
un @tokei-hexlab[pielikumus]). un @tokei-hexlab[pielikumus]).
Saskaņā ar QSM etalontabulu "Business Systems Implementation Unit (New and Saskaņā ar QSM etalontabulu "Business Systems Implementation Unit (New and
Modified IU) Benchmarks", pirmās kvartiles projekti ($25%$ mazākie no $550$ Modified IU) Benchmarks", pirmās kvartiles projekti ($25%$ mazākie no $550$
biznesa sistēmu projektiem) vidēji ilgst $3.2$ mēnešus, ar vidēji $1.57$ biznesa sistēmu projektiem) vidēji ilgst $3.2$ mēnešus, ar vidēji $1.57$
izstrādātājiem un mediāno projekta apjomu -- $1889$ koda rindiņas. izstrādātājiem un mediāno projekta apjomu -- $1889$ koda rindiņas @QSM.
Ņemot vērā, ka projekta autors ir students ar ierobežotu pieredzi, tiek Ņemot vērā, ka projekta autors ir students ar ierobežotu pieredzi, tiek
izmantota pirmās kvartiles $50%$ diapazona augšējā robeža -- $466$ rindiņas izmantota pirmās kvartiles $50%$ diapazona augšējā robeža -- $466$ rindiņas
personmēnesī. personmēnesī.
@ -2118,8 +2109,8 @@ Projekta turpmākās attīstības iespējas ietver:
) )
#include "attachments.typ" #include "attachments.typ"
// #include "code.typ" #include "code.typ"
#include "doc.typ" #include "doc.typ"
#pagebreak() // #pagebreak()
#total-words words // #total-words words

View File

@ -142,11 +142,20 @@
) )
} }
#let codeblock(filename, lang) = { #let codeblock(caption, filename, lang: "rust") = {
raw( show figure: set block(breakable: true)
read(filename), show raw: set par.line(numbering: "1")
block: true, set figure(numbering: "1")
lang: lang,
figure(
caption: caption,
kind: "attachment",
supplement: "pielikums",
raw(
read(filename),
block: true,
lang: lang,
),
) )
} }