diff --git a/src/layout.rs b/src/layout.rs new file mode 100644 index 0000000..6296a19 --- /dev/null +++ b/src/layout.rs @@ -0,0 +1,134 @@ +#[derive(Debug, Clone)] +pub struct Room { + pub x: usize, + pub y: usize, + pub width: usize, + pub height: usize, +} + +impl Room { + pub fn center_cell(&self) -> (usize, usize) { + (self.x + (self.width / 2), self.y + (self.height / 2)) + } +} + +#[derive(Debug, Clone)] +pub struct Corridor { + pub from: (usize, usize), + pub to: (usize, usize), +} + +#[derive(Debug, Clone, Default)] +pub struct DungeonLayout { + pub rooms: Vec, + pub corridors: Vec, +} + +pub fn generate_layout(cols: usize, rows: usize, target_room_count: usize, seed: u64) -> DungeonLayout { + let mut rng = SimpleRng::new(seed ^ ((cols as u64) << 32) ^ rows as u64); + let mut rooms = Vec::new(); + let mut corridors = Vec::new(); + + let min_room_size = 2; + if cols < min_room_size || rows < min_room_size || target_room_count == 0 { + return DungeonLayout { rooms, corridors }; + } + + let max_room_width = (cols / 3).clamp(min_room_size, cols.min(12)); + let max_room_height = (rows / 3).clamp(min_room_size, rows.min(12)); + let max_attempts = target_room_count.saturating_mul(40).max(50); + + for _ in 0..max_attempts { + if rooms.len() >= target_room_count { + break; + } + + let width = rng.range_inclusive(min_room_size, max_room_width); + let height = rng.range_inclusive(min_room_size, max_room_height); + let x = rng.range_inclusive(0, cols - width); + let y = rng.range_inclusive(0, rows - height); + + let candidate = Room { + x, + y, + width, + height, + }; + + if rooms + .iter() + .all(|existing| !overlaps_with_padding(&candidate, existing, 1)) + { + rooms.push(candidate); + } + } + + for idx in 1..rooms.len() { + let a = rooms[idx - 1].center_cell(); + let b = rooms[idx].center_cell(); + + if rng.next_bool() { + push_corridor_if_nonzero(&mut corridors, (a.0, a.1), (b.0, a.1)); + push_corridor_if_nonzero(&mut corridors, (b.0, a.1), (b.0, b.1)); + } else { + push_corridor_if_nonzero(&mut corridors, (a.0, a.1), (a.0, b.1)); + push_corridor_if_nonzero(&mut corridors, (a.0, b.1), (b.0, b.1)); + } + } + + DungeonLayout { rooms, corridors } +} + +fn overlaps_with_padding(a: &Room, b: &Room, padding: usize) -> bool { + let a_left = a.x.saturating_sub(padding); + let a_top = a.y.saturating_sub(padding); + let a_right = a.x + a.width + padding; + let a_bottom = a.y + a.height + padding; + + let b_left = b.x; + let b_top = b.y; + let b_right = b.x + b.width; + let b_bottom = b.y + b.height; + + a_left < b_right && a_right > b_left && a_top < b_bottom && a_bottom > b_top +} + +fn push_corridor_if_nonzero( + corridors: &mut Vec, + from: (usize, usize), + to: (usize, usize), +) { + if from != to { + corridors.push(Corridor { from, to }); + } +} + +struct SimpleRng { + state: u64, +} + +impl SimpleRng { + fn new(seed: u64) -> Self { + let state = if seed == 0 { 0xA5A5_A5A5_1234_5678 } else { seed }; + Self { state } + } + + fn next_u32(&mut self) -> u32 { + self.state ^= self.state >> 12; + self.state ^= self.state << 25; + self.state ^= self.state >> 27; + (self.state.wrapping_mul(0x2545_F491_4F6C_DD1D) >> 32) as u32 + } + + fn next_bool(&mut self) -> bool { + (self.next_u32() & 1) == 0 + } + + fn range_inclusive(&mut self, min: usize, max: usize) -> usize { + if min >= max { + return min; + } + let width = max - min + 1; + min + (self.next_u32() as usize % width) + } +} diff --git a/src/main.rs b/src/main.rs index d12db1b..2a4a29e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,8 @@ +mod layout; + use eframe::egui; use egui::{Color32, Stroke}; +use layout::{DungeonLayout, generate_layout}; fn main() -> eframe::Result<()> { let options = eframe::NativeOptions::default(); @@ -14,16 +17,33 @@ fn main() -> eframe::Result<()> { struct DungeonApp { cols: usize, rows: usize, + room_count: usize, + seed: u64, + layout: DungeonLayout, } impl Default for DungeonApp { fn default() -> Self { - Self { cols: 12, rows: 8 } + let cols = 24; + let rows = 18; + let room_count = 8; + let seed = 1; + let layout = generate_layout(cols, rows, room_count, seed); + + Self { + cols, + rows, + room_count, + seed, + layout, + } } } impl eframe::App for DungeonApp { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + let mut settings_changed = false; + egui::SidePanel::left("options_panel") .resizable(true) .min_width(180.0) @@ -35,36 +55,77 @@ impl eframe::App for DungeonApp { ui.label("Columns"); ui.horizontal(|ui| { - ui.add(egui::Slider::new(&mut self.cols, 2..=100).show_value(false)); - ui.add( - egui::DragValue::new(&mut self.cols) - .speed(1.0) - .range(2..=500), - ); + settings_changed |= ui + .add(egui::Slider::new(&mut self.cols, 6..=100).show_value(false)) + .changed(); + settings_changed |= ui + .add( + egui::DragValue::new(&mut self.cols) + .speed(1.0) + .range(2..=500), + ) + .changed(); }); ui.add_space(8.0); ui.label("Rows"); ui.horizontal(|ui| { - ui.add(egui::Slider::new(&mut self.rows, 2..=100).show_value(false)); - ui.add( - egui::DragValue::new(&mut self.rows) - .speed(1.0) - .range(2..=500), - ); + settings_changed |= ui + .add(egui::Slider::new(&mut self.rows, 6..=100).show_value(false)) + .changed(); + settings_changed |= ui + .add( + egui::DragValue::new(&mut self.rows) + .speed(1.0) + .range(2..=500), + ) + .changed(); }); + + ui.add_space(12.0); + ui.heading("Dungeon"); + ui.add_space(8.0); + ui.label("Room Count"); + ui.horizontal(|ui| { + settings_changed |= ui + .add(egui::Slider::new(&mut self.room_count, 1..=40).show_value(false)) + .changed(); + settings_changed |= ui + .add( + egui::DragValue::new(&mut self.room_count) + .speed(1.0) + .range(1..=200), + ) + .changed(); + }); + + ui.add_space(10.0); + if ui.button("Generate New Layout").clicked() { + self.seed = self.seed.wrapping_add(1); + self.layout = generate_layout(self.cols, self.rows, self.room_count, self.seed); + } }); + if settings_changed { + self.layout = generate_layout(self.cols, self.rows, self.room_count, self.seed); + } + egui::CentralPanel::default().show(ctx, |ui| { let available = ui.available_size_before_wrap(); let (response, painter) = ui.allocate_painter(available, egui::Sense::hover()); let canvas = response.rect.shrink(12.0); - draw_grid(&painter, canvas, self.cols, self.rows); + let geometry = draw_grid(&painter, canvas, self.cols, self.rows); + draw_layout(&painter, &geometry, &self.layout); }); } } -fn draw_grid(painter: &egui::Painter, area: egui::Rect, cols: usize, rows: usize) { +struct GridGeometry { + rect: egui::Rect, + cell_size: f32, +} + +fn draw_grid(painter: &egui::Painter, area: egui::Rect, cols: usize, rows: usize) -> GridGeometry { let cols = cols.max(1); let rows = rows.max(1); @@ -104,4 +165,62 @@ fn draw_grid(painter: &egui::Painter, area: egui::Rect, cols: usize, rows: usize stroke, ); } + + GridGeometry { + rect: grid_rect, + cell_size, + } +} + +fn draw_layout(painter: &egui::Painter, geometry: &GridGeometry, layout: &DungeonLayout) { + let room_fill = Color32::from_rgb(70, 120, 160); + let room_stroke = Stroke::new(1.0, Color32::from_rgb(35, 65, 95)); + let corridor_fill = Color32::from_rgb(210, 190, 120); + + for corridor in &layout.corridors { + for (col, row) in corridor_cells(corridor.from, corridor.to) { + let cell_rect = cell_rect(geometry, col, row); + painter.rect_filled(cell_rect, 0.0, corridor_fill); + } + } + + for room in &layout.rooms { + let left = geometry.rect.left() + room.x as f32 * geometry.cell_size; + let top = geometry.rect.top() + room.y as f32 * geometry.cell_size; + let right = left + room.width as f32 * geometry.cell_size; + let bottom = top + room.height as f32 * geometry.cell_size; + let room_rect = egui::Rect::from_min_max(egui::pos2(left, top), egui::pos2(right, bottom)); + + painter.rect_filled(room_rect, 0.0, room_fill); + painter.rect_stroke(room_rect, 0.0, room_stroke, egui::StrokeKind::Middle); + } +} + +fn corridor_cells(from: (usize, usize), to: (usize, usize)) -> Vec<(usize, usize)> { + let mut cells = Vec::new(); + if from.0 == to.0 { + let x = from.0; + let start = from.1.min(to.1); + let end = from.1.max(to.1); + for y in start..=end { + cells.push((x, y)); + } + } else if from.1 == to.1 { + let y = from.1; + let start = from.0.min(to.0); + let end = from.0.max(to.0); + for x in start..=end { + cells.push((x, y)); + } + } + cells +} + +fn cell_rect(geometry: &GridGeometry, col: usize, row: usize) -> egui::Rect { + let left = geometry.rect.left() + col as f32 * geometry.cell_size; + let top = geometry.rect.top() + row as f32 * geometry.cell_size; + egui::Rect::from_min_size( + egui::pos2(left, top), + egui::vec2(geometry.cell_size, geometry.cell_size), + ) }