diff --git a/src/exporter.rs b/src/exporter.rs index 247cab4..dee90b6 100644 --- a/src/exporter.rs +++ b/src/exporter.rs @@ -6,7 +6,7 @@ use image::{DynamicImage, ImageFormat, RgbaImage, imageops::FilterType}; use rfd::FileDialog; use tiny_skia::{FillRule, Paint, PathBuilder, Pixmap, Rect, Stroke, StrokeDash, Transform}; -use crate::layout::{Door, DungeonLayout, Room}; +use crate::layout::{Door, DungeonLayout, Room, corridor_cells}; use crate::ui::{ExportFormat, MaskFormat, UiSettings}; const BG: (u8, u8, u8, u8) = (24, 24, 26, 255); @@ -83,24 +83,21 @@ struct SceneData { corridor_cells: HashSet<(usize, usize)>, corridor_edges: HashSet<((usize, usize), (usize, usize))>, room_cells: HashSet<(usize, usize)>, + room_edges: HashSet<((usize, usize), (usize, usize))>, door_edges: HashSet<((usize, usize), (usize, usize))>, } -fn collect_scene_data(layout: &DungeonLayout) -> SceneData { +fn collect_scene_data(layout: &DungeonLayout, settings: &UiSettings) -> SceneData { let mut scene = SceneData::default(); for door in &layout.doors { - scene.door_edges.insert(norm_edge(door.from, door.to)); + for edge in door_edges_for(door, settings.cols, settings.rows) { + scene.door_edges.insert(edge); + } } - for corridor in &layout.corridors { - for &cell in &corridor.path { - scene.corridor_cells.insert(cell); - } - for pair in corridor.path.windows(2) { - scene.corridor_edges.insert(norm_edge(pair[0], pair[1])); - } - } + scene.corridor_cells = corridor_cells(layout, settings.cols, settings.rows); + scene.corridor_edges = corridor_edges_from_cells(&scene.corridor_cells); for room in &layout.rooms { for x in room.x..(room.x + room.width) { @@ -109,6 +106,7 @@ fn collect_scene_data(layout: &DungeonLayout) -> SceneData { } } } + scene.room_edges = room_edges_from_rooms(&layout.rooms); scene } @@ -168,7 +166,7 @@ pub fn export_with_dialog( fn render_pixmap(layout: &DungeonLayout, settings: &UiSettings) -> Result { let g = ExportGeometry::new(settings.cols, settings.rows); - let scene = collect_scene_data(layout); + let scene = collect_scene_data(layout, settings); let mut pixmap = Pixmap::new(g.width, g.height).ok_or_else(|| "Failed to allocate canvas".to_string())?; fill_bg(&mut pixmap, BG); @@ -206,7 +204,7 @@ fn render_pixmap(layout: &DungeonLayout, settings: &UiSettings) -> Result String { ); } - let mut corridor_cells = HashSet::new(); - let mut corridor_edges = HashSet::new(); + let corridor_cells = corridor_cells(layout, settings.cols, settings.rows); + let corridor_edges = corridor_edges_from_cells(&corridor_cells); let mut room_cells = HashSet::new(); + let room_edges = room_edges_from_rooms(&layout.rooms); let mut door_edges = HashSet::new(); for door in &layout.doors { - door_edges.insert(norm_edge(door.from, door.to)); - } - for corridor in &layout.corridors { - for &cell in &corridor.path { - corridor_cells.insert(cell); - } - for pair in corridor.path.windows(2) { - corridor_edges.insert(norm_edge(pair[0], pair[1])); + for edge in door_edges_for(door, settings.cols, settings.rows) { + door_edges.insert(edge); } } + // corridor_cells/corridor_edges already populated with corridor widths for room in &layout.rooms { for x in room.x..(room.x + room.width) { for y in room.y..(room.y + room.height) { @@ -486,7 +480,14 @@ fn build_svg(layout: &DungeonLayout, settings: &UiSettings) -> String { &door_edges, wall_w, ); - append_svg_walls(&mut s, &g, &room_cells, None, &door_edges, wall_w); + append_svg_walls( + &mut s, + &g, + &room_cells, + Some(&room_edges), + &door_edges, + wall_w, + ); for door in &layout.doors { let (color, dash) = if settings.colorblind_mode { @@ -505,7 +506,7 @@ fn build_svg(layout: &DungeonLayout, settings: &UiSettings) -> String { ("rgb(220,70,70)", None) }; - if let Some((x1, y1, x2, y2)) = door_line_points(&g, door.from, door.to) { + if let Some((x1, y1, x2, y2)) = door_line_points(&g, door.from, door.to, door.width) { if let Some(pattern) = dash { let _ = writeln!( s, @@ -847,7 +848,7 @@ fn draw_door( color: (u8, u8, u8, u8), style: DoorStyle, ) { - let Some((x1, y1, x2, y2)) = door_line_points(g, door.from, door.to) else { + let Some((x1, y1, x2, y2)) = door_line_points(g, door.from, door.to, door.width) else { return; }; draw_line(pixmap, x1, y1, x2, y2, width, color, style); @@ -857,22 +858,31 @@ fn door_line_points( g: &ExportGeometry, a: (usize, usize), b: (usize, usize), + width_cells: usize, ) -> Option<(f32, f32, f32, f32)> { if a.0.abs_diff(b.0) + a.1.abs_diff(b.1) != 1 { return None; } + let width_cells = width_cells.max(1) as isize; + let min_offset = -((width_cells - 1) / 2); + let max_offset = width_cells / 2; + if a.0 != b.0 { let x = g.left() + (a.0.max(b.0) as f32) * g.cell; - let row = a.1; - let y0 = g.top() + row as f32 * g.cell; - let y1 = y0 + g.cell; + let row = a.1 as isize; + let y0_cell = (row + min_offset).clamp(0, g.rows.saturating_sub(1) as isize); + let y1_cell = (row + max_offset).clamp(0, g.rows.saturating_sub(1) as isize); + let y0 = g.top() + y0_cell as f32 * g.cell; + let y1 = g.top() + (y1_cell as f32 + 1.0) * g.cell; Some((x, y0, x, y1)) } else { let y = g.top() + (a.1.max(b.1) as f32) * g.cell; - let col = a.0; - let x0 = g.left() + col as f32 * g.cell; - let x1 = x0 + g.cell; + let col = a.0 as isize; + let x0_cell = (col + min_offset).clamp(0, g.cols.saturating_sub(1) as isize); + let x1_cell = (col + max_offset).clamp(0, g.cols.saturating_sub(1) as isize); + let x0 = g.left() + x0_cell as f32 * g.cell; + let x1 = g.left() + (x1_cell as f32 + 1.0) * g.cell; Some((x0, y, x1, y)) } } @@ -954,3 +964,76 @@ fn dotted_line( fn norm_edge(a: (usize, usize), b: (usize, usize)) -> ((usize, usize), (usize, usize)) { if a <= b { (a, b) } else { (b, a) } } + +fn corridor_edges_from_cells( + corridor_cells: &HashSet<(usize, usize)>, +) -> HashSet<((usize, usize), (usize, usize))> { + let mut edges = HashSet::new(); + for &(col, row) in corridor_cells { + let right = (col + 1, row); + let bottom = (col, row + 1); + if corridor_cells.contains(&right) { + edges.insert(norm_edge((col, row), right)); + } + if corridor_cells.contains(&bottom) { + edges.insert(norm_edge((col, row), bottom)); + } + } + edges +} + +fn room_edges_from_rooms(rooms: &[Room]) -> HashSet<((usize, usize), (usize, usize))> { + let mut edges = HashSet::new(); + for room in rooms { + let x_end = room.x + room.width; + let y_end = room.y + room.height; + for x in room.x..x_end { + for y in room.y..y_end { + if x + 1 < x_end { + edges.insert(norm_edge((x, y), (x + 1, y))); + } + if y + 1 < y_end { + edges.insert(norm_edge((x, y), (x, y + 1))); + } + } + } + } + edges +} + +fn door_edges_for(door: &Door, cols: usize, rows: usize) -> Vec<((usize, usize), (usize, usize))> { + let mut edges = Vec::new(); + if door.from.0.abs_diff(door.to.0) + door.from.1.abs_diff(door.to.1) != 1 { + return edges; + } + + let width = door.width.max(1) as isize; + let min_offset = -((width - 1) / 2); + let max_offset = width / 2; + + if door.from.0 != door.to.0 { + let y = door.from.1 as isize; + for dy in min_offset..=max_offset { + let ny = y + dy; + if ny < 0 || ny >= rows as isize { + continue; + } + let a = (door.from.0, ny as usize); + let b = (door.to.0, ny as usize); + edges.push(norm_edge(a, b)); + } + } else { + let x = door.from.0 as isize; + for dx in min_offset..=max_offset { + let nx = x + dx; + if nx < 0 || nx >= cols as isize { + continue; + } + let a = (nx as usize, door.from.1); + let b = (nx as usize, door.to.1); + edges.push(norm_edge(a, b)); + } + } + + edges +} diff --git a/src/layout.rs b/src/layout.rs index 5c7f521..5384142 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -23,12 +23,14 @@ pub struct Corridor { pub start_room_id: usize, pub end_room_id: usize, pub path: Vec<(usize, usize)>, + pub width: usize, } #[derive(Debug, Clone)] pub struct Door { pub from: (usize, usize), pub to: (usize, usize), + pub width: usize, pub locked: bool, pub archway: bool, } @@ -56,6 +58,8 @@ pub fn generate_layout( min_room_size: usize, max_room_size: usize, square_rooms_only: bool, + min_corridor_width: usize, + max_corridor_width: usize, corridor_randomness_percent: usize, dead_end_room_percent: usize, door_settings: DoorSettings, @@ -65,12 +69,15 @@ pub fn generate_layout( ^ ((target_room_count as u64) << 16) ^ (min_room_size as u64) ^ ((max_room_size as u64) << 8) + ^ ((min_corridor_width as u64) << 24) + ^ ((max_corridor_width as u64) << 28) ^ ((corridor_randomness_percent as u64) << 56) ^ ((dead_end_room_percent as u64) << 40); let base_seed = seed::derive_seed(seed, layout_salt); let mut room_rng = SimpleRng::new(seed::derive_seed(base_seed, 1)); let mut graph_rng = SimpleRng::new(seed::derive_seed(base_seed, 2)); let mut path_rng = SimpleRng::new(seed::derive_seed(base_seed, 3)); + let mut corridor_rng = SimpleRng::new(seed::derive_seed(base_seed, 4)); let mut rooms = Vec::new(); let mut corridors = Vec::new(); @@ -155,6 +162,10 @@ pub fn generate_layout( } let randomness = (corridor_randomness_percent.min(100) as f32) / 100.0; + let mut min_width = min_corridor_width.max(1); + let max_grid_width = cols.max(1).min(rows.max(1)); + let max_width = max_corridor_width.max(min_width).min(max_grid_width); + min_width = min_width.min(max_width); let centers: Vec<(usize, usize)> = rooms.iter().map(Room::center_cell).collect(); let target_dead_end_rooms = ((rooms.len() * dead_end_room_percent.min(50)) + 50) / 100; let room_edges = @@ -187,6 +198,7 @@ pub fn generate_layout( start_room_id, end_room_id, path, + width: corridor_rng.range_inclusive(min_width, max_width), }); next_corridor_id = next_corridor_id.wrapping_add(1); } @@ -197,11 +209,17 @@ pub fn generate_layout( corridors, doors: Vec::new(), }; - apply_doors(&mut layout, seed, door_settings); + apply_doors(&mut layout, seed, door_settings, cols, rows); layout } -pub fn apply_doors(layout: &mut DungeonLayout, seed: u64, settings: DoorSettings) { +pub fn apply_doors( + layout: &mut DungeonLayout, + seed: u64, + settings: DoorSettings, + cols: usize, + rows: usize, +) { layout.doors.clear(); let mut rng = SimpleRng::new(seed::derive_seed(seed, 0xD005_5EED_u64)); @@ -209,40 +227,67 @@ pub fn apply_doors(layout: &mut DungeonLayout, seed: u64, settings: DoorSettings let room_hall = (settings.room_hallway_percent.min(100) as f32) / 100.0; let locked = (settings.locked_percent.min(100) as f32) / 100.0; + let corridor_cells = corridor_cells(layout, cols, rows); let mut seen_edges = HashSet::new(); + let door_chance = base * room_hall; for corridor in &layout.corridors { - for pair in corridor.path.windows(2) { - if pair[0] == pair[1] { - continue; - } - let edge = normalized_cell_edge(pair[0], pair[1]); - if !seen_edges.insert(edge) { - continue; - } + if corridor.path.len() < 2 { + continue; + } - let a_in_room = room_index_at_cell(&layout.rooms, edge.0).is_some(); - let b_in_room = room_index_at_cell(&layout.rooms, edge.1).is_some(); - let is_room_hallway = a_in_room ^ b_in_room; - let is_middle = !a_in_room && !b_in_room; + let start_room = &layout.rooms[corridor.start_room_id]; + let end_room = &layout.rooms[corridor.end_room_id]; - if is_room_hallway { - let door_chance = base * room_hall; + if let Some(edge) = room_exit_edge(&corridor.path, start_room, true) { + if seen_edges.insert(edge) { let place_door = door_chance > 0.0 && rng.next_f32() <= door_chance; - layout.doors.push(Door { from: edge.0, to: edge.1, + width: corridor.width.max(1), locked: place_door && rng.next_f32() <= locked, archway: !place_door, }); - continue; } + } - if is_middle && settings.allow_middle_corridor_doors { - if base > 0.0 && rng.next_f32() <= base { + if let Some(edge) = room_exit_edge(&corridor.path, end_room, false) { + if seen_edges.insert(edge) { + let place_door = door_chance > 0.0 && rng.next_f32() <= door_chance; + layout.doors.push(Door { + from: edge.0, + to: edge.1, + width: corridor.width.max(1), + locked: place_door && rng.next_f32() <= locked, + archway: !place_door, + }); + } + } + } + + if settings.allow_middle_corridor_doors { + for &(x, y) in &corridor_cells { + let right = (x + 1, y); + let bottom = (x, y + 1); + if x + 1 < cols && corridor_cells.contains(&right) { + let edge = normalized_cell_edge((x, y), right); + if seen_edges.insert(edge) && base > 0.0 && rng.next_f32() <= base { layout.doors.push(Door { from: edge.0, to: edge.1, + width: 1, + locked: rng.next_f32() <= locked, + archway: false, + }); + } + } + if y + 1 < rows && corridor_cells.contains(&bottom) { + let edge = normalized_cell_edge((x, y), bottom); + if seen_edges.insert(edge) && base > 0.0 && rng.next_f32() <= base { + layout.doors.push(Door { + from: edge.0, + to: edge.1, + width: 1, locked: rng.next_f32() <= locked, archway: false, }); @@ -252,6 +297,115 @@ pub fn apply_doors(layout: &mut DungeonLayout, seed: u64, settings: DoorSettings } } +fn room_exit_edge( + path: &[(usize, usize)], + room: &Room, + from_start: bool, +) -> Option<((usize, usize), (usize, usize))> { + let in_room = |cell: (usize, usize)| { + cell.0 >= room.x + && cell.0 < room.x + room.width + && cell.1 >= room.y + && cell.1 < room.y + room.height + }; + + if from_start { + for pair in path.windows(2) { + if in_room(pair[0]) && !in_room(pair[1]) { + return Some(normalized_cell_edge(pair[0], pair[1])); + } + } + } else { + for pair in path.windows(2).rev() { + if in_room(pair[1]) && !in_room(pair[0]) { + return Some(normalized_cell_edge(pair[0], pair[1])); + } + } + } + + None +} + +pub fn corridor_cells(layout: &DungeonLayout, cols: usize, rows: usize) -> HashSet<(usize, usize)> { + let mut cells = HashSet::new(); + if cols == 0 || rows == 0 { + return cells; + } + + let mut room_cells = HashSet::new(); + for room in &layout.rooms { + for x in room.x..(room.x + room.width) { + for y in room.y..(room.y + room.height) { + room_cells.insert((x, y)); + } + } + } + + for corridor in &layout.corridors { + let width = corridor.width.max(1); + let min_offset = -((width as isize - 1) / 2); + let max_offset = width as isize / 2; + if corridor.path.len() == 1 { + let (x, y) = corridor.path[0]; + for dy in min_offset..=max_offset { + let ny = y as isize + dy; + if ny < 0 || ny >= rows as isize { + continue; + } + let cell = (x, ny as usize); + if !room_cells.contains(&cell) { + cells.insert(cell); + } + } + continue; + } + + for pair in corridor.path.windows(2) { + let a = pair[0]; + let b = pair[1]; + if a == b { + continue; + } + + if a.0 != b.0 { + let x0 = a.0.min(b.0); + let x1 = a.0.max(b.0); + let y = a.1 as isize; + for x in x0..=x1 { + for dy in min_offset..=max_offset { + let ny = y + dy; + if ny < 0 || ny >= rows as isize { + continue; + } + let cell = (x, ny as usize); + if !room_cells.contains(&cell) { + cells.insert(cell); + } + } + } + } else { + let y0 = a.1.min(b.1); + let y1 = a.1.max(b.1); + let x = a.0 as isize; + for y in y0..=y1 { + for dx in min_offset..=max_offset { + let nx = x + dx; + if nx < 0 || nx >= cols as isize { + continue; + } + let cell = (nx as usize, y); + if !room_cells.contains(&cell) { + cells.insert(cell); + } + } + } + } + } + } + + cells +} + fn build_room_connection_edges( centers: &[(usize, usize)], randomness: f32, @@ -557,15 +711,6 @@ fn manhattan_distance(a: (usize, usize), b: (usize, usize)) -> usize { a.0.abs_diff(b.0) + a.1.abs_diff(b.1) } -fn room_index_at_cell(rooms: &[Room], cell: (usize, usize)) -> Option { - rooms.iter().position(|room| { - cell.0 >= room.x - && cell.0 < room.x + room.width - && cell.1 >= room.y - && cell.1 < room.y + room.height - }) -} - fn normalized_cell_edge(a: (usize, usize), b: (usize, usize)) -> ((usize, usize), (usize, usize)) { if a <= b { (a, b) } else { (b, a) } } diff --git a/src/main.rs b/src/main.rs index 853a3bb..1cbb2f3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,7 +8,7 @@ use std::collections::HashSet; use eframe::egui; use egui::{Color32, Stroke}; -use layout::{DoorSettings, DungeonLayout, shortest_path_cells}; +use layout::{DoorSettings, DungeonLayout, corridor_cells, shortest_path_cells}; use ui::{UiSettings, draw_side_panel}; fn main() -> eframe::Result<()> { @@ -38,6 +38,8 @@ impl Default for DungeonApp { settings.min_room_size, settings.max_room_size, settings.square_rooms_only, + settings.min_corridor_width, + settings.max_corridor_width, settings.corridor_randomness, settings.dead_end_rooms_percent, door_settings_from_ui(&settings), @@ -65,6 +67,12 @@ impl eframe::App for DungeonApp { if self.settings.min_room_size > self.settings.max_room_size { self.settings.max_room_size = self.settings.min_room_size; } + if self.settings.min_corridor_width == 0 { + self.settings.min_corridor_width = 1; + } + if self.settings.min_corridor_width > self.settings.max_corridor_width { + self.settings.max_corridor_width = self.settings.min_corridor_width; + } if panel_result.reset_clicked || panel_result.settings_changed { self.regenerate_layout(); @@ -133,6 +141,8 @@ impl DungeonApp { self.settings.min_room_size, self.settings.max_room_size, self.settings.square_rooms_only, + self.settings.min_corridor_width, + self.settings.max_corridor_width, self.settings.corridor_randomness, self.settings.dead_end_rooms_percent, door_settings_from_ui(&self.settings), @@ -306,6 +316,8 @@ impl DungeonApp { &mut self.layout, self.settings.seed, door_settings_from_ui(&self.settings), + self.settings.cols, + self.settings.rows, ); } } @@ -333,6 +345,8 @@ enum DragState { struct GridGeometry { rect: egui::Rect, cell_size: f32, + cols: usize, + rows: usize, } fn draw_grid(painter: &egui::Painter, area: egui::Rect, cols: usize, rows: usize) -> GridGeometry { @@ -379,6 +393,8 @@ fn draw_grid(painter: &egui::Painter, area: egui::Rect, cols: usize, rows: usize GridGeometry { rect: grid_rect, cell_size, + cols, + rows, } } @@ -394,21 +410,15 @@ fn draw_layout( let room_fill = Color32::from_rgb(70, 120, 160); let corridor_fill = Color32::from_rgb(210, 190, 120); let wall_color = Color32::BLACK; - let mut corridor_cells_set = HashSet::new(); - let mut corridor_edges_set = HashSet::new(); + let corridor_cells_set = corridor_cells(layout, geometry.cols, geometry.rows); + let corridor_edges_set = corridor_edges_from_cells(&corridor_cells_set); let mut room_cells_set = HashSet::new(); + let room_edges_set = room_edges_from_rooms(&layout.rooms); let mut door_edges_set = HashSet::new(); for door in &layout.doors { - door_edges_set.insert(normalized_edge(door.from, door.to)); - } - - for corridor in &layout.corridors { - for &cell in &corridor.path { - corridor_cells_set.insert(cell); - } - for pair in corridor.path.windows(2) { - corridor_edges_set.insert(normalized_edge(pair[0], pair[1])); + for edge in door_edges_for(door, geometry.cols, geometry.rows) { + door_edges_set.insert(edge); } } @@ -510,12 +520,15 @@ fn draw_layout( let left = col.checked_sub(1).map(|x| (x, row)); let top = row.checked_sub(1).map(|y| (col, y)); - if col == 0 + let left_edge = col == 0 || (!room_cells_set.contains(&(col - 1, row)) + || left + .map(|n| !room_edges_set.contains(&normalized_edge((col, row), n))) + .unwrap_or(true)) && left .map(|n| !door_edges_set.contains(&normalized_edge((col, row), n))) - .unwrap_or(true)) - { + .unwrap_or(true); + if left_edge { draw_wall_segment( painter, egui::pos2(rect.left(), rect.top()), @@ -524,7 +537,8 @@ fn draw_layout( wall_color, ); } - if !room_cells_set.contains(&right) + if (!room_cells_set.contains(&right) + || !room_edges_set.contains(&normalized_edge((col, row), right))) && !door_edges_set.contains(&normalized_edge((col, row), right)) { draw_wall_segment( @@ -535,12 +549,15 @@ fn draw_layout( wall_color, ); } - if row == 0 + let top_edge = row == 0 || (!room_cells_set.contains(&(col, row - 1)) + || top + .map(|n| !room_edges_set.contains(&normalized_edge((col, row), n))) + .unwrap_or(true)) && top .map(|n| !door_edges_set.contains(&normalized_edge((col, row), n))) - .unwrap_or(true)) - { + .unwrap_or(true); + if top_edge { draw_wall_segment( painter, egui::pos2(rect.left(), rect.top()), @@ -549,7 +566,8 @@ fn draw_layout( wall_color, ); } - if !room_cells_set.contains(&bottom) + if (!room_cells_set.contains(&bottom) + || !room_edges_set.contains(&normalized_edge((col, row), bottom))) && !door_edges_set.contains(&normalized_edge((col, row), bottom)) { draw_wall_segment( @@ -588,6 +606,7 @@ fn draw_layout( geometry, door.from, door.to, + door.width, Stroke::new(door_width_px, color), style, ); @@ -636,6 +655,83 @@ fn normalized_edge(a: (usize, usize), b: (usize, usize)) -> ((usize, usize), (us if a <= b { (a, b) } else { (b, a) } } +fn corridor_edges_from_cells( + corridor_cells: &HashSet<(usize, usize)>, +) -> HashSet<((usize, usize), (usize, usize))> { + let mut edges = HashSet::new(); + for &(col, row) in corridor_cells { + let right = (col + 1, row); + let bottom = (col, row + 1); + if corridor_cells.contains(&right) { + edges.insert(normalized_edge((col, row), right)); + } + if corridor_cells.contains(&bottom) { + edges.insert(normalized_edge((col, row), bottom)); + } + } + edges +} + +fn room_edges_from_rooms(rooms: &[layout::Room]) -> HashSet<((usize, usize), (usize, usize))> { + let mut edges = HashSet::new(); + for room in rooms { + let x_end = room.x + room.width; + let y_end = room.y + room.height; + for x in room.x..x_end { + for y in room.y..y_end { + if x + 1 < x_end { + edges.insert(normalized_edge((x, y), (x + 1, y))); + } + if y + 1 < y_end { + edges.insert(normalized_edge((x, y), (x, y + 1))); + } + } + } + } + edges +} + +fn door_edges_for( + door: &layout::Door, + cols: usize, + rows: usize, +) -> Vec<((usize, usize), (usize, usize))> { + let mut edges = Vec::new(); + if door.from.0.abs_diff(door.to.0) + door.from.1.abs_diff(door.to.1) != 1 { + return edges; + } + + let width = door.width.max(1) as isize; + let min_offset = -((width - 1) / 2); + let max_offset = width / 2; + + if door.from.0 != door.to.0 { + let y = door.from.1 as isize; + for dy in min_offset..=max_offset { + let ny = y + dy; + if ny < 0 || ny >= rows as isize { + continue; + } + let a = (door.from.0, ny as usize); + let b = (door.to.0, ny as usize); + edges.push(normalized_edge(a, b)); + } + } else { + let x = door.from.0 as isize; + for dx in min_offset..=max_offset { + let nx = x + dx; + if nx < 0 || nx >= cols as isize { + continue; + } + let a = (nx as usize, door.from.1); + let b = (nx as usize, door.to.1); + edges.push(normalized_edge(a, b)); + } + } + + edges +} + fn pointer_to_grid(pointer_pos: egui::Pos2, geometry: &GridGeometry) -> Option<(f32, f32)> { if !geometry.rect.contains(pointer_pos) { return None; @@ -726,6 +822,7 @@ fn draw_door_line( geometry: &GridGeometry, a: (usize, usize), b: (usize, usize), + width_cells: usize, stroke: Stroke, style: DoorLineStyle, ) { @@ -733,17 +830,25 @@ fn draw_door_line( return; } + let width_cells = width_cells.max(1); + let min_offset = -((width_cells as isize - 1) / 2); + let max_offset = width_cells as isize / 2; + if a.0 != b.0 { let x = geometry.rect.left() + (a.0.max(b.0) as f32) * geometry.cell_size; - let row = a.1; - let y0 = geometry.rect.top() + row as f32 * geometry.cell_size; - let y1 = y0 + geometry.cell_size; + let row = a.1 as isize; + let y0_cell = (row + min_offset).clamp(0, geometry.rows.saturating_sub(1) as isize); + let y1_cell = (row + max_offset).clamp(0, geometry.rows.saturating_sub(1) as isize); + let y0 = geometry.rect.top() + y0_cell as f32 * geometry.cell_size; + let y1 = geometry.rect.top() + (y1_cell as f32 + 1.0) * geometry.cell_size; draw_styled_line(painter, egui::pos2(x, y0), egui::pos2(x, y1), stroke, style); } else { let y = geometry.rect.top() + (a.1.max(b.1) as f32) * geometry.cell_size; - let col = a.0; - let x0 = geometry.rect.left() + col as f32 * geometry.cell_size; - let x1 = x0 + geometry.cell_size; + let col = a.0 as isize; + let x0_cell = (col + min_offset).clamp(0, geometry.cols.saturating_sub(1) as isize); + let x1_cell = (col + max_offset).clamp(0, geometry.cols.saturating_sub(1) as isize); + let x0 = geometry.rect.left() + x0_cell as f32 * geometry.cell_size; + let x1 = geometry.rect.left() + (x1_cell as f32 + 1.0) * geometry.cell_size; draw_styled_line(painter, egui::pos2(x0, y), egui::pos2(x1, y), stroke, style); } } diff --git a/src/ui.rs b/src/ui.rs index 8f9b82e..a04cd9f 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -97,6 +97,8 @@ pub struct UiSettings { pub min_room_size: usize, pub max_room_size: usize, pub square_rooms_only: bool, + pub min_corridor_width: usize, + pub max_corridor_width: usize, pub corridor_randomness: usize, pub dead_end_rooms_percent: usize, pub door_frequency_percent: usize, @@ -122,6 +124,8 @@ impl Default for UiSettings { min_room_size: 2, max_room_size: 6, square_rooms_only: false, + min_corridor_width: 1, + max_corridor_width: 2, corridor_randomness: 0, dead_end_rooms_percent: 0, door_frequency_percent: 30, @@ -403,6 +407,36 @@ fn draw_layout_tab(ui: &mut egui::Ui, settings: &mut UiSettings, result: &mut Si .changed(); }); + ui.add_space(8.0); + ui.label("Min Corridor Width"); + ui.horizontal(|ui| { + result.settings_changed |= ui + .add(egui::Slider::new(&mut settings.min_corridor_width, 1..=10).show_value(false)) + .changed(); + result.settings_changed |= ui + .add( + egui::DragValue::new(&mut settings.min_corridor_width) + .speed(1.0) + .range(1..=50), + ) + .changed(); + }); + + ui.add_space(8.0); + ui.label("Max Corridor Width"); + ui.horizontal(|ui| { + result.settings_changed |= ui + .add(egui::Slider::new(&mut settings.max_corridor_width, 1..=10).show_value(false)) + .changed(); + result.settings_changed |= ui + .add( + egui::DragValue::new(&mut settings.max_corridor_width) + .speed(1.0) + .range(1..=50), + ) + .changed(); + }); + ui.add_space(8.0); ui.label("Dead-End Rooms (%)"); ui.horizontal(|ui| {