From 2d3758941edbdaed5105b0ac383fa12ef2767716 Mon Sep 17 00:00:00 2001 From: grimsace Date: Mon, 23 Mar 2026 12:43:33 -0500 Subject: [PATCH] added secret doors --- src/exporter.rs | 31 ++++++++++--- src/layout.rs | 121 ++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 85 +++++++++++++++++++++++++++++++--- src/ui.rs | 24 ++++++++++ 4 files changed, 248 insertions(+), 13 deletions(-) diff --git a/src/exporter.rs b/src/exporter.rs index c2ace94..b9860b7 100644 --- a/src/exporter.rs +++ b/src/exporter.rs @@ -16,6 +16,7 @@ const CORRIDOR: (u8, u8, u8, u8) = (210, 190, 120, 255); const BLACK: (u8, u8, u8, u8) = (0, 0, 0, 255); const OPEN_DOOR: (u8, u8, u8, u8) = (80, 200, 120, 255); const LOCKED_DOOR: (u8, u8, u8, u8) = (220, 70, 70, 255); +const SECRET_DOOR: (u8, u8, u8, u8) = (170, 80, 170, 255); const ARCHWAY: (u8, u8, u8, u8) = (230, 140, 60, 255); const WINDOW: (u8, u8, u8, u8) = (70, 130, 220, 255); const WHITE: (u8, u8, u8, u8) = (255, 255, 255, 255); @@ -32,6 +33,7 @@ enum DoorStyle { ShortDash, Dotted, DashDot, + DashDotDot, } #[derive(Debug, Clone, Copy)] @@ -241,7 +243,9 @@ fn render_pixmap(layout: &DungeonLayout, settings: &UiSettings) -> Result Result String { for door in &layout.doors { let (color, dash) = if settings.colorblind_mode { - if door.archway { + if door.secret { + ("black", Some("7,3,1,3,1,4")) + } else if door.archway { ("black", Some("2,4")) } else if door.locked { ("black", Some("6,4")) } else { ("black", Some("14,7")) } + } else if door.secret { + ("rgb(170,80,170)", None) } else if door.archway { - ("rgb(70,130,220)", None) + ("rgb(230,140,60)", None) } else if door.locked { - ("rgb(80,200,120)", None) - } else { ("rgb(220,70,70)", None) + } else { + ("rgb(80,200,120)", None) }; if let Some((x1, y1, x2, y2)) = @@ -1117,6 +1133,9 @@ fn draw_line( DoorStyle::ShortDash => stroke_path(pixmap, x1, y1, x2, y2, width, color, Some((6.0, 4.0))), DoorStyle::Dotted => dotted_line(pixmap, x1, y1, x2, y2, width, color), DoorStyle::DashDot => stroke_path(pixmap, x1, y1, x2, y2, width, color, Some((7.0, 3.0))), + DoorStyle::DashDotDot => { + stroke_path(pixmap, x1, y1, x2, y2, width, color, Some((7.0, 3.0))) + } } } diff --git a/src/layout.rs b/src/layout.rs index 894fe3a..01f2744 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -35,6 +35,7 @@ pub struct Door { pub span_width: bool, pub locked: bool, pub archway: bool, + pub secret: bool, } #[derive(Debug, Clone)] @@ -58,6 +59,7 @@ pub struct DoorSettings { pub frequency_percent: usize, pub room_hallway_percent: usize, pub locked_percent: usize, + pub secret_percent: usize, pub allow_middle_corridor_doors: bool, } @@ -359,6 +361,7 @@ pub fn apply_doors( span_width: true, locked: place_door && rng.next_f32() <= locked, archway: !place_door, + secret: false, }); } } @@ -373,6 +376,7 @@ pub fn apply_doors( span_width: true, locked: place_door && rng.next_f32() <= locked, archway: !place_door, + secret: false, }); } } @@ -392,6 +396,7 @@ pub fn apply_doors( span_width: true, locked: place_door && rng.next_f32() <= locked, archway: !place_door, + secret: false, }); } } @@ -412,6 +417,7 @@ pub fn apply_doors( span_width: false, locked: rng.next_f32() <= locked, archway: false, + secret: false, }); } } @@ -425,11 +431,14 @@ pub fn apply_doors( span_width: false, locked: rng.next_f32() <= locked, archway: false, + secret: false, }); } } } } + + apply_secret_doors(layout, seed, settings); } fn apply_packed_room_doors(layout: &mut DungeonLayout, seed: u64, settings: DoorSettings) { @@ -460,8 +469,116 @@ fn apply_packed_room_doors(layout: &mut DungeonLayout, seed: u64, settings: Door span_width: width > 1, locked: place_door && rng.next_f32() <= locked, archway: !place_door, + secret: false, }); } + + apply_secret_doors(layout, seed, settings); +} + +fn apply_secret_doors(layout: &mut DungeonLayout, seed: u64, settings: DoorSettings) { + let secret_percent = settings.secret_percent.min(100); + let chance = (secret_percent as f32) / 100.0; + if chance <= 0.0 || layout.rooms.is_empty() || layout.doors.is_empty() { + return; + } + + let mut rng = SimpleRng::new(seed::derive_seed(seed, 0x5EC2_E700_u64)); + let room_door_map = room_to_door_indices(layout); + let eligible_secret_doors = (0..layout.doors.len()) + .filter(|&door_idx| { + let door = &layout.doors[door_idx]; + !door.archway + && !door.locked + && door_is_secret_eligible_for_rooms(layout, &room_door_map, door_idx) + }) + .collect::>(); + + for (door_idx, door) in layout.doors.iter_mut().enumerate() { + if !eligible_secret_doors.contains(&door_idx) { + continue; + } + + if rng.next_f32() <= chance { + door.locked = false; + door.archway = false; + door.secret = true; + } + } +} + +fn door_is_secret_eligible_for_rooms( + layout: &DungeonLayout, + room_door_map: &[Vec], + door_idx: usize, +) -> bool { + for door_indices in room_door_map { + if !door_indices.contains(&door_idx) { + continue; + } + + for &other_door_idx in door_indices { + if other_door_idx == door_idx { + continue; + } + + if !door_directly_connects_rooms(layout, other_door_idx) { + return false; + } + } + } + + true +} + +fn room_to_door_indices(layout: &DungeonLayout) -> Vec> { + let mut room_door_map = vec![Vec::new(); layout.rooms.len()]; + for (door_idx, door) in layout.doors.iter().enumerate() { + for (room_idx, room) in layout.rooms.iter().enumerate() { + if door_touches_room(door, room) { + room_door_map[room_idx].push(door_idx); + } + } + } + room_door_map +} + +fn secret_room_ids(layout: &DungeonLayout) -> HashSet { + let mut secret_rooms = HashSet::new(); + for door in layout.doors.iter().filter(|door| door.secret) { + for (room_idx, room) in layout.rooms.iter().enumerate() { + if door_touches_room(door, room) { + secret_rooms.insert(room_idx); + } + } + } + secret_rooms +} + +fn door_touches_room(door: &Door, room: &Room) -> bool { + cell_in_room(door.from, room) || cell_in_room(door.to, room) +} + +fn door_directly_connects_rooms(layout: &DungeonLayout, door_idx: usize) -> bool { + let Some(door) = layout.doors.get(door_idx) else { + return false; + }; + + let touching_rooms = layout + .rooms + .iter() + .enumerate() + .filter_map(|(room_idx, room)| door_touches_room(door, room).then_some(room_idx)) + .collect::>(); + + touching_rooms.len() >= 2 +} + +fn cell_in_room(cell: (usize, usize), room: &Room) -> bool { + cell.0 >= room.x + && cell.0 < room.x + room.width + && cell.1 >= room.y + && cell.1 < room.y + room.height } pub fn apply_windows( @@ -532,9 +649,13 @@ fn collect_window_segments( allow_internal_windows: bool, ) -> Vec { let corridor_cells = corridor_cells(layout, cols, rows); + let secret_rooms = secret_room_ids(layout); let mut segments = Vec::new(); for (room_idx, room) in layout.rooms.iter().enumerate() { + if secret_rooms.contains(&room_idx) { + continue; + } collect_room_side_segments( room_idx, room, diff --git a/src/main.rs b/src/main.rs index 7d9b50b..aef4632 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,6 +16,13 @@ use layout::{ }; use ui::{AddTool, UiSettings, draw_side_panel}; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum ManualDoorKind { + Regular, + Locked, + Secret, +} + // Boot the native app and start the egui event loop. fn main() -> eframe::Result<()> { let options = eframe::NativeOptions::default(); @@ -164,6 +171,7 @@ impl eframe::App for DungeonApp { draw_legend_entry_colorblind(ui, "Walls", LegendStyle::SolidLine); draw_legend_entry_colorblind(ui, "Doors", LegendStyle::LongDash); draw_legend_entry_colorblind(ui, "Locked Doors", LegendStyle::ShortDash); + draw_legend_entry_colorblind(ui, "Secret Doors", LegendStyle::DashDotDotLine); draw_legend_entry_colorblind(ui, "Archways", LegendStyle::DottedLine); draw_legend_entry_colorblind(ui, "Windows", LegendStyle::DashDotLine); } else { @@ -172,6 +180,7 @@ impl eframe::App for DungeonApp { draw_legend_entry(ui, Color32::BLACK, "Walls"); draw_legend_entry(ui, Color32::from_rgb(80, 200, 120), "Doors"); draw_legend_entry(ui, Color32::from_rgb(220, 70, 70), "Locked Doors"); + draw_legend_entry(ui, Color32::from_rgb(170, 80, 170), "Secret Doors"); draw_legend_entry(ui, Color32::from_rgb(230, 140, 60), "Archways"); draw_legend_entry(ui, Color32::from_rgb(70, 130, 220), "Windows"); } @@ -511,13 +520,18 @@ impl DungeonApp { self.settings.add_tool = AddTool::None; } } - AddTool::Door | AddTool::LockedDoor => { + AddTool::Door | AddTool::LockedDoor | AddTool::SecretDoor => { self.add_corridor_drag = None; if response.clicked() && let Some(pointer_pos) = response.interact_pointer_pos() { - let locked = self.settings.add_tool == AddTool::LockedDoor; - self.add_door_at_pointer(pointer_pos, geometry, locked); + let kind = match self.settings.add_tool { + AddTool::Door => ManualDoorKind::Regular, + AddTool::LockedDoor => ManualDoorKind::Locked, + AddTool::SecretDoor => ManualDoorKind::Secret, + _ => unreachable!(), + }; + self.add_door_at_pointer(pointer_pos, geometry, kind); self.settings.add_tool = AddTool::None; } } @@ -926,11 +940,13 @@ impl DungeonApp { span_width: false, locked: false, archway: true, + secret: false, }); } let door = &mut self.layout.doors[idx]; door.locked = false; door.archway = true; + door.secret = false; door.width = 1; door.span_width = false; self.layout.doors.extend(spawned); @@ -978,7 +994,7 @@ impl DungeonApp { &mut self, pointer_pos: egui::Pos2, geometry: &GridGeometry, - locked: bool, + kind: ManualDoorKind, ) { let Some(edge) = self.door_edge_at_pointer(pointer_pos, geometry) else { return; @@ -1014,11 +1030,13 @@ impl DungeonApp { span_width: false, locked: false, archway: true, + secret: false, }); } let door = &mut self.layout.doors[idx]; - door.locked = locked; + door.locked = kind == ManualDoorKind::Locked; door.archway = false; + door.secret = kind == ManualDoorKind::Secret; door.width = 1; door.span_width = false; self.layout.doors.extend(spawned); @@ -1043,8 +1061,9 @@ impl DungeonApp { to: edge.1, width: 1, span_width: false, - locked, + locked: kind == ManualDoorKind::Locked, archway: false, + secret: kind == ManualDoorKind::Secret, }); } @@ -1385,6 +1404,8 @@ fn draw_layout( for door in &layout.doors { let color = if colorblind_mode { Color32::BLACK + } else if door.secret { + Color32::from_rgb(170, 80, 170) } else if door.archway { Color32::from_rgb(230, 140, 60) } else if door.locked { @@ -1393,7 +1414,9 @@ fn draw_layout( Color32::from_rgb(80, 200, 120) }; let style = if colorblind_mode { - if door.archway { + if door.secret { + DoorLineStyle::DashDotDot + } else if door.archway { DoorLineStyle::Dotted } else if door.locked { DoorLineStyle::ShortDash @@ -1736,6 +1759,7 @@ fn door_settings_from_ui(settings: &UiSettings) -> DoorSettings { frequency_percent: settings.door_frequency_percent, room_hallway_percent: settings.room_hallway_door_percent, locked_percent: settings.locked_door_percent, + secret_percent: settings.secret_door_percent, allow_middle_corridor_doors: settings.allow_middle_corridor_doors, } } @@ -1846,6 +1870,7 @@ enum DoorLineStyle { ShortDash, Dotted, DashDot, + DashDotDot, } // Draw a line with solid, dashed, or dotted styling. @@ -1864,6 +1889,7 @@ fn draw_styled_line( DoorLineStyle::ShortDash => draw_dashed_line(painter, from, to, stroke, 6.0, 4.0), DoorLineStyle::Dotted => draw_dotted_line(painter, from, to, stroke), DoorLineStyle::DashDot => draw_dash_dot_line(painter, from, to, stroke), + DoorLineStyle::DashDotDot => draw_dash_dot_dot_line(painter, from, to, stroke), } } @@ -1943,6 +1969,44 @@ fn draw_dash_dot_line(painter: &egui::Painter, from: egui::Pos2, to: egui::Pos2, } } +fn draw_dash_dot_dot_line( + painter: &egui::Painter, + from: egui::Pos2, + to: egui::Pos2, + stroke: Stroke, +) { + let dx = to.x - from.x; + let dy = to.y - from.y; + let len = (dx * dx + dy * dy).sqrt(); + if len <= 0.0 { + return; + } + + let ux = dx / len; + let uy = dy / len; + let radius = (stroke.width * 0.35).max(1.0); + let mut dist = 0.0_f32; + while dist < len { + let dash_end = (dist + 7.0).min(len); + let p0 = egui::pos2(from.x + ux * dist, from.y + uy * dist); + let p1 = egui::pos2(from.x + ux * dash_end, from.y + uy * dash_end); + painter.line_segment([p0, p1], stroke); + dist = dash_end + 3.0; + if dist >= len { + break; + } + let dot1 = egui::pos2(from.x + ux * dist, from.y + uy * dist); + painter.circle_filled(dot1, radius, stroke.color); + dist += 3.0; + if dist >= len { + break; + } + let dot2 = egui::pos2(from.x + ux * dist, from.y + uy * dist); + painter.circle_filled(dot2, radius, stroke.color); + dist += 4.0; + } +} + // Compute the center position of a grid cell. fn cell_center(geometry: &GridGeometry, col: usize, row: usize) -> egui::Pos2 { egui::pos2( @@ -1993,6 +2057,7 @@ enum LegendStyle { ShortDash, DottedLine, DashDotLine, + DashDotDotLine, } // Draw a patterned legend entry for colorblind mode. @@ -2069,6 +2134,12 @@ fn draw_legend_entry_colorblind(ui: &mut egui::Ui, label: &str, style: LegendSty egui::pos2(rect.right() - 1.0, rect.center().y), Stroke::new(2.0, Color32::BLACK), ), + LegendStyle::DashDotDotLine => draw_dash_dot_dot_line( + ui.painter(), + egui::pos2(rect.left() + 1.0, rect.center().y), + egui::pos2(rect.right() - 1.0, rect.center().y), + Stroke::new(2.0, Color32::BLACK), + ), } ui.add_space(6.0); diff --git a/src/ui.rs b/src/ui.rs index 6570a5f..9bfb904 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -114,6 +114,7 @@ pub struct UiSettings { pub door_frequency_percent: usize, pub room_hallway_door_percent: usize, pub locked_door_percent: usize, + pub secret_door_percent: usize, pub allow_middle_corridor_doors: bool, pub windows_enabled: bool, pub min_window_width: usize, @@ -151,6 +152,7 @@ impl Default for UiSettings { door_frequency_percent: 30, room_hallway_door_percent: 70, locked_door_percent: 25, + secret_door_percent: 10, allow_middle_corridor_doors: false, windows_enabled: false, min_window_width: 1, @@ -178,6 +180,7 @@ pub enum AddTool { Corridor, Door, LockedDoor, + SecretDoor, } #[derive(Debug, Clone, Copy, Default)] @@ -581,6 +584,21 @@ fn draw_layout_tab(ui: &mut egui::Ui, settings: &mut UiSettings, result: &mut Si .changed(); }); + ui.add_space(8.0); + ui.label("Secret Door Chance (%)"); + ui.horizontal(|ui| { + result.settings_changed |= ui + .add(egui::Slider::new(&mut settings.secret_door_percent, 0..=100).show_value(false)) + .changed(); + result.settings_changed |= ui + .add( + egui::DragValue::new(&mut settings.secret_door_percent) + .speed(1.0) + .range(0..=100), + ) + .changed(); + }); + ui.add_space(8.0); result.settings_changed |= ui .checkbox( @@ -690,6 +708,11 @@ fn draw_add_tab(ui: &mut egui::Ui, settings: &mut UiSettings) { AddTool::LockedDoor, "Add Locked Door", ); + ui.selectable_value( + &mut settings.add_tool, + AddTool::SecretDoor, + "Add Secret Door", + ); }); ui.add_space(6.0); @@ -698,6 +721,7 @@ fn draw_add_tab(ui: &mut egui::Ui, settings: &mut UiSettings) { AddTool::Corridor => ui.label("Click-drag to place a corridor between two cells."), AddTool::Door => ui.label("Click a room/corridor edge to place a door."), AddTool::LockedDoor => ui.label("Click a room/corridor edge to place a locked door."), + AddTool::SecretDoor => ui.label("Click a room/corridor edge to place a secret door."), AddTool::None => ui.label("Select a tool to add rooms or corridors."), }; }