added secret doors

This commit is contained in:
grimsace
2026-03-23 12:43:33 -05:00
parent bdd1e931c3
commit 2d3758941e
4 changed files with 248 additions and 13 deletions
+25 -6
View File
@@ -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<Pixmap
for door in &layout.doors {
let (color, style) = if settings.colorblind_mode {
let s = if door.archway {
let s = if door.secret {
DoorStyle::DashDotDot
} else if door.archway {
DoorStyle::Dotted
} else if door.locked {
DoorStyle::ShortDash
@@ -249,6 +253,8 @@ fn render_pixmap(layout: &DungeonLayout, settings: &UiSettings) -> Result<Pixmap
DoorStyle::LongDash
};
(BLACK, s)
} else if door.secret {
(SECRET_DOOR, DoorStyle::Solid)
} else if door.archway {
(ARCHWAY, DoorStyle::Solid)
} else if door.locked {
@@ -322,14 +328,19 @@ fn export_composite_masks(
.ok_or_else(|| "Failed to allocate archways mask canvas".to_string())?;
let mut locked_doors = Pixmap::new(g.width, g.height)
.ok_or_else(|| "Failed to allocate locked doors mask canvas".to_string())?;
let mut secret_doors = Pixmap::new(g.width, g.height)
.ok_or_else(|| "Failed to allocate secret doors mask canvas".to_string())?;
let mut windows = Pixmap::new(g.width, g.height)
.ok_or_else(|| "Failed to allocate windows mask canvas".to_string())?;
fill_bg(&mut doors, BLACK);
fill_bg(&mut archways, BLACK);
fill_bg(&mut locked_doors, BLACK);
fill_bg(&mut secret_doors, BLACK);
fill_bg(&mut windows, BLACK);
for door in &layout.doors {
if door.archway {
if door.secret {
draw_door(&mut secret_doors, &g, door, door_w, WHITE, DoorStyle::Solid);
} else if door.archway {
draw_door(&mut archways, &g, door, door_w, WHITE, DoorStyle::Solid);
} else if door.locked {
draw_door(&mut locked_doors, &g, door, door_w, WHITE, DoorStyle::Solid);
@@ -354,6 +365,7 @@ fn export_composite_masks(
save_mask_image(folder, "doors_mask", &doors, settings)?;
save_mask_image(folder, "archways_mask", &archways, settings)?;
save_mask_image(folder, "locked_doors_mask", &locked_doors, settings)?;
save_mask_image(folder, "secret_doors_mask", &secret_doors, settings)?;
save_mask_image(folder, "windows_mask", &windows, settings)?;
if settings.export_show_grid {
let mut grid = Pixmap::new(g.width, g.height)
@@ -601,19 +613,23 @@ fn build_svg(layout: &DungeonLayout, settings: &UiSettings) -> 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)))
}
}
}
+121
View File
@@ -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::<HashSet<_>>();
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<usize>],
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<Vec<usize>> {
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<usize> {
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::<Vec<_>>();
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<WindowSegment> {
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,
+78 -7
View File
@@ -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);
+24
View File
@@ -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."),
};
}