diff --git a/README.md b/README.md index 203b38a..3fc4db1 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,11 @@ A Rust desktop app for generating simple tabletop dungeon layouts. - Corridor controls: - `Corridor Randomness (%)` from `0` to `100` - `Dead-End Rooms (%)` from `0` to `50` +- Door controls: + - `Door Frequency (%)` + - `Room/Hallway Door Chance (%)` + - `Locked Door Chance (%)` + - `Allow Middle Corridor Doors` toggle - Generate control: - Master seed input (`u64`) - `Random` seed button @@ -28,6 +33,10 @@ A Rust desktop app for generating simple tabletop dungeon layouts. - Corridors are stored separately as vector line segments between grid cells. - Every generated room is connected into one navigable network when possible. - A master seed drives deterministic generation, and internal subsystems derive their own sub-seeds from it. +- Doors are generated from corridor adjacency: + - Open doors are rendered in red + - Locked doors are rendered in green + - Doors are rendered as line segments on grid boundaries ### Corridor Randomness diff --git a/src/layout.rs b/src/layout.rs index 7b12f07..5c7f521 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -25,10 +25,27 @@ pub struct Corridor { pub path: Vec<(usize, usize)>, } +#[derive(Debug, Clone)] +pub struct Door { + pub from: (usize, usize), + pub to: (usize, usize), + pub locked: bool, + pub archway: bool, +} + +#[derive(Debug, Clone, Copy)] +pub struct DoorSettings { + pub frequency_percent: usize, + pub room_hallway_percent: usize, + pub locked_percent: usize, + pub allow_middle_corridor_doors: bool, +} + #[derive(Debug, Clone, Default)] pub struct DungeonLayout { pub rooms: Vec, pub corridors: Vec, + pub doors: Vec, } pub fn generate_layout( @@ -41,6 +58,7 @@ pub fn generate_layout( square_rooms_only: bool, corridor_randomness_percent: usize, dead_end_room_percent: usize, + door_settings: DoorSettings, ) -> DungeonLayout { let layout_salt = ((cols as u64) << 48) ^ ((rows as u64) << 32) @@ -58,7 +76,11 @@ pub fn generate_layout( let mut corridors = Vec::new(); if cols < 2 || rows < 2 || target_room_count == 0 { - return DungeonLayout { rooms, corridors }; + return DungeonLayout { + rooms, + corridors, + doors: Vec::new(), + }; } let mut min_size = min_room_size.max(2); @@ -74,7 +96,11 @@ pub fn generate_layout( } if min_size == 0 || max_size < min_size { - return DungeonLayout { rooms, corridors }; + return DungeonLayout { + rooms, + corridors, + doors: Vec::new(), + }; } let max_attempts = target_room_count.saturating_mul(40).max(50); @@ -121,7 +147,11 @@ pub fn generate_layout( } if rooms.len() < 2 { - return DungeonLayout { rooms, corridors }; + return DungeonLayout { + rooms, + corridors, + doors: Vec::new(), + }; } let randomness = (corridor_randomness_percent.min(100) as f32) / 100.0; @@ -162,7 +192,64 @@ pub fn generate_layout( } } - DungeonLayout { rooms, corridors } + let mut layout = DungeonLayout { + rooms, + corridors, + doors: Vec::new(), + }; + apply_doors(&mut layout, seed, door_settings); + layout +} + +pub fn apply_doors(layout: &mut DungeonLayout, seed: u64, settings: DoorSettings) { + layout.doors.clear(); + + let mut rng = SimpleRng::new(seed::derive_seed(seed, 0xD005_5EED_u64)); + let base = (settings.frequency_percent.min(100) as f32) / 100.0; + 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 mut seen_edges = HashSet::new(); + 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; + } + + 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; + + if is_room_hallway { + let door_chance = base * room_hall; + let place_door = door_chance > 0.0 && rng.next_f32() <= door_chance; + + layout.doors.push(Door { + from: edge.0, + to: edge.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 { + layout.doors.push(Door { + from: edge.0, + to: edge.1, + locked: rng.next_f32() <= locked, + archway: false, + }); + } + } + } + } } fn build_room_connection_edges( @@ -470,6 +557,19 @@ 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) } +} + struct SimpleRng { state: u64, } diff --git a/src/main.rs b/src/main.rs index 69718f9..329ac25 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,7 +7,7 @@ use std::collections::HashSet; use eframe::egui; use egui::{Color32, Stroke}; -use layout::{DungeonLayout, shortest_path_cells}; +use layout::{DoorSettings, DungeonLayout, shortest_path_cells}; use ui::{UiSettings, draw_side_panel}; fn main() -> eframe::Result<()> { @@ -39,6 +39,7 @@ impl Default for DungeonApp { settings.square_rooms_only, settings.corridor_randomness, settings.dead_end_rooms_percent, + door_settings_from_ui(&settings), ); Self { settings, @@ -92,6 +93,7 @@ impl DungeonApp { self.settings.square_rooms_only, self.settings.corridor_randomness, self.settings.dead_end_rooms_percent, + door_settings_from_ui(&self.settings), ); } @@ -185,6 +187,7 @@ impl DungeonApp { self.layout.rooms[drag.room_idx].x = new_x; self.layout.rooms[drag.room_idx].y = new_y; self.reroute_corridors_for_room(drag.room_idx); + self.refresh_doors(); } fn reroute_corridors_for_room(&mut self, room_idx: usize) { @@ -253,6 +256,15 @@ impl DungeonApp { }; self.layout.corridors[drag.corridor_index].path = new_path; + self.refresh_doors(); + } + + fn refresh_doors(&mut self) { + layout::apply_doors( + &mut self.layout, + self.settings.seed, + door_settings_from_ui(&self.settings), + ); } } @@ -405,6 +417,23 @@ fn draw_layout(painter: &egui::Painter, geometry: &GridGeometry, layout: &Dungeo painter.rect_filled(room_rect, 0.0, room_fill); painter.rect_stroke(room_rect, 0.0, outline_stroke, egui::StrokeKind::Middle); } + + for door in &layout.doors { + let color = if door.archway { + Color32::from_rgb(70, 130, 220) + } else if door.locked { + Color32::from_rgb(80, 200, 120) + } else { + Color32::from_rgb(220, 70, 70) + }; + draw_door_line( + painter, + geometry, + door.from, + door.to, + Stroke::new(3.0, color), + ); + } } fn cell_rect(geometry: &GridGeometry, col: usize, row: usize) -> egui::Rect { @@ -495,3 +524,38 @@ fn simplify_path_loops(path: &mut Vec<(usize, usize)>) { } *path = out; } + +fn door_settings_from_ui(settings: &UiSettings) -> DoorSettings { + DoorSettings { + frequency_percent: settings.door_frequency_percent, + room_hallway_percent: settings.room_hallway_door_percent, + locked_percent: settings.locked_door_percent, + allow_middle_corridor_doors: settings.allow_middle_corridor_doors, + } +} + +fn draw_door_line( + painter: &egui::Painter, + geometry: &GridGeometry, + a: (usize, usize), + b: (usize, usize), + stroke: Stroke, +) { + if a.0.abs_diff(b.0) + a.1.abs_diff(b.1) != 1 { + return; + } + + 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; + painter.line_segment([egui::pos2(x, y0), egui::pos2(x, y1)], stroke); + } 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; + painter.line_segment([egui::pos2(x0, y), egui::pos2(x1, y)], stroke); + } +} diff --git a/src/ui.rs b/src/ui.rs index c9f2cf3..4eb888f 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -27,6 +27,10 @@ pub struct UiSettings { pub square_rooms_only: bool, pub corridor_randomness: usize, pub dead_end_rooms_percent: usize, + pub door_frequency_percent: usize, + pub room_hallway_door_percent: usize, + pub locked_door_percent: usize, + pub allow_middle_corridor_doors: bool, active_tab: Tab, } @@ -42,6 +46,10 @@ impl Default for UiSettings { square_rooms_only: false, corridor_randomness: 0, dead_end_rooms_percent: 0, + door_frequency_percent: 30, + room_hallway_door_percent: 70, + locked_door_percent: 25, + allow_middle_corridor_doors: false, active_tab: Tab::Generate, } } @@ -227,4 +235,65 @@ fn draw_layout_tab(ui: &mut egui::Ui, settings: &mut UiSettings, result: &mut Si ) .changed(); }); + + ui.add_space(12.0); + ui.separator(); + ui.add_space(8.0); + ui.label(RichText::new("Door Settings").strong()); + ui.add_space(8.0); + + ui.label("Door Frequency (%)"); + ui.horizontal(|ui| { + result.settings_changed |= ui + .add(egui::Slider::new(&mut settings.door_frequency_percent, 0..=100).show_value(false)) + .changed(); + result.settings_changed |= ui + .add( + egui::DragValue::new(&mut settings.door_frequency_percent) + .speed(1.0) + .range(0..=100), + ) + .changed(); + }); + + ui.add_space(8.0); + ui.label("Room/Hallway Door Chance (%)"); + ui.horizontal(|ui| { + result.settings_changed |= ui + .add( + egui::Slider::new(&mut settings.room_hallway_door_percent, 0..=100) + .show_value(false), + ) + .changed(); + result.settings_changed |= ui + .add( + egui::DragValue::new(&mut settings.room_hallway_door_percent) + .speed(1.0) + .range(0..=100), + ) + .changed(); + }); + + ui.add_space(8.0); + ui.label("Locked Door Chance (%)"); + ui.horizontal(|ui| { + result.settings_changed |= ui + .add(egui::Slider::new(&mut settings.locked_door_percent, 0..=100).show_value(false)) + .changed(); + result.settings_changed |= ui + .add( + egui::DragValue::new(&mut settings.locked_door_percent) + .speed(1.0) + .range(0..=100), + ) + .changed(); + }); + + ui.add_space(8.0); + result.settings_changed |= ui + .checkbox( + &mut settings.allow_middle_corridor_doors, + "Allow Middle Corridor Doors", + ) + .changed(); }