diff --git a/src/exporter.rs b/src/exporter.rs
index 64191c7..918fb0f 100644
--- a/src/exporter.rs
+++ b/src/exporter.rs
@@ -826,51 +826,32 @@ pub fn build_svg(layout: &DungeonLayout, settings: &UiSettings) -> String {
settings.colorblind_mode,
);
- // Draw stairs and elevators.
+ // Draw stairs.
for stair in &layout.stairs {
let x = g.left() + stair.cell.0 as f32 * g.cell;
let y = g.top() + stair.cell.1 as f32 * g.cell;
let w = stair.size as f32 * g.cell;
let h = stair.size as f32 * g.cell;
- if stair.is_elevator {
- let fill = if settings.colorblind_mode {
- "rgb(180,180,180)"
- } else {
- "rgb(100,150,200)"
- };
- let _ = writeln!(
- s,
- ""
- );
- let _ = writeln!(
- s,
- "E",
- cx = x + w / 2.0,
- cy = y + h / 2.0,
- fs = (g.cell * stair.size as f32 * 0.4).clamp(14.0, 48.0)
- );
+ let fill = if settings.colorblind_mode {
+ "rgb(180,180,180)"
} else {
- let fill = if settings.colorblind_mode {
- "rgb(180,180,180)"
- } else {
- "rgb(200,150,50)"
- };
+ "rgb(200,150,50)"
+ };
+ let _ = writeln!(
+ s,
+ ""
+ );
+ let steps = (stair.size as f32 * 2.0).round() as usize;
+ let step_h = h / steps as f32;
+ for i in 0..steps {
+ let sy = y + (i as f32 * step_h) + step_h * 0.5;
let _ = writeln!(
s,
- ""
+ "",
+ x1 = x + 2.0,
+ x2 = x + w - 2.0
);
- let steps = (stair.size as f32 * 2.0).round() as usize;
- let step_h = h / steps as f32;
- for i in 0..steps {
- let sy = y + (i as f32 * step_h) + step_h * 0.5;
- let _ = writeln!(
- s,
- "",
- x1 = x + 2.0,
- x2 = x + w - 2.0
- );
- }
}
}
diff --git a/src/layout.rs b/src/layout.rs
index 7e0fe93..f441256 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -65,7 +65,6 @@ pub struct AreaMarker {
pub struct Staircase {
pub cell: (usize, usize),
pub size: usize,
- pub is_elevator: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
diff --git a/src/main.rs b/src/main.rs
index 9cd5a2f..b1876cf 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -2323,11 +2323,9 @@ fn draw_staircase(
let bottom = top + stair.size as f32 * geometry.cell_size;
let rect = egui::Rect::from_min_max(egui::pos2(left, top), egui::pos2(right, bottom));
- // Fill with a distinctive color for stairs/elevators.
+ // Fill with a distinctive color for stairs.
let fill = if colorblind_mode {
Color32::from_rgb(180, 180, 180).gamma_multiply(0.4)
- } else if stair.is_elevator {
- Color32::from_rgb(100, 150, 200).gamma_multiply(0.35)
} else {
Color32::from_rgb(200, 150, 50).gamma_multiply(0.35)
};
@@ -2336,8 +2334,6 @@ fn draw_staircase(
// Draw border.
let stroke_color = if colorblind_mode {
Color32::BLACK
- } else if stair.is_elevator {
- Color32::from_rgb(100, 150, 200)
} else {
Color32::from_rgb(200, 150, 50)
};
@@ -2348,42 +2344,23 @@ fn draw_staircase(
egui::StrokeKind::Middle,
);
- // Draw stair lines or elevator symbol.
- if stair.is_elevator {
- // Draw elevator symbol: a rectangle with arrows.
- let center = rect.center();
- let text = "E";
- let font_size = (geometry.cell_size * stair.size as f32 * 0.4).clamp(14.0, 48.0);
- painter.text(
- center,
- egui::Align2::CENTER_CENTER,
- text,
- egui::FontId::proportional(font_size),
- if colorblind_mode {
- Color32::BLACK
- } else {
- Color32::WHITE
- },
- );
+ // Draw stair lines (horizontal steps).
+ let steps = (stair.size as f32 * 2.0).round() as usize;
+ let step_height = (rect.height() / (steps as f32)).max(1.0);
+ let line_color = if colorblind_mode {
+ Color32::BLACK
} else {
- // Draw stair lines (horizontal steps).
- let steps = (stair.size as f32 * 2.0).round() as usize;
- let step_height = (rect.height() / (steps as f32)).max(1.0);
- let line_color = if colorblind_mode {
- Color32::BLACK
- } else {
- Color32::from_rgb(150, 100, 30)
- };
- for i in 0..steps {
- let y = rect.top() + (i as f32 * step_height) + step_height * 0.5;
- painter.line_segment(
- [
- egui::pos2(rect.left() + 2.0, y),
- egui::pos2(rect.right() - 2.0, y),
- ],
- Stroke::new(1.5, line_color),
- );
- }
+ Color32::from_rgb(150, 100, 30)
+ };
+ for i in 0..steps {
+ let y = rect.top() + (i as f32 * step_height) + step_height * 0.5;
+ painter.line_segment(
+ [
+ egui::pos2(rect.left() + 2.0, y),
+ egui::pos2(rect.right() - 2.0, y),
+ ],
+ Stroke::new(1.5, line_color),
+ );
}
}
diff --git a/src/startend.rs b/src/startend.rs
index a9f9d73..de12dbc 100644
--- a/src/startend.rs
+++ b/src/startend.rs
@@ -353,70 +353,171 @@ fn pick_random_corridor_cell(
// Constant for stair marker stream base
const STAIR_MARKER_STREAM_BASE: u64 = 21_000;
-// Populate stairs (and optionally elevators) across all levels after markers are placed.
+// Populate stairs across all levels after markers are placed.
pub fn populate_stairs(
mut layouts: Vec,
settings: &UiSettings,
) -> Vec {
+ if layouts.is_empty() {
+ return layouts;
+ }
+
+ // Clear existing stairs.
+ for layout in &mut layouts {
+ layout.stairs.clear();
+ }
+
if settings.min_stairs_per_level == 0 && settings.max_stairs_per_level == 0 {
return layouts;
}
- let stair_count = if settings.min_stairs_per_level == settings.max_stairs_per_level {
- settings.min_stairs_per_level
+ let num_levels = layouts.len();
+ let num_gaps = num_levels.saturating_sub(1);
+
+ // If we only have 1 level, we still generate one set of stairs (e.g. to a hypothetical level below).
+ let sets_to_gen = if num_gaps == 0 { 1 } else { num_gaps };
+ let mut all_stair_sets: Vec> = Vec::new();
+
+ if settings.sync_stairs_across_levels {
+ // One set of positions for all gaps.
+ let stair_count = get_stair_count(settings, 0);
+ let base_layout = &layouts[0];
+ let synced_stairs = pick_stair_positions(base_layout, settings, stair_count, 0);
+ for _ in 0..sets_to_gen {
+ all_stair_sets.push(synced_stairs.clone());
+ }
} else {
- let range_seed = seed::derive_seed(settings.seed, 0x2A_3B_4_u64);
- (range_seed as usize % (settings.max_stairs_per_level - settings.min_stairs_per_level + 1))
- + settings.min_stairs_per_level
- };
-
- if stair_count == 0 {
- return layouts;
+ // Independent positions for each gap.
+ for i in 0..sets_to_gen {
+ let stair_count = get_stair_count(settings, i);
+ // Use the layout of the upper level of the gap as a guide.
+ let layout_idx = i.min(num_levels - 1);
+ let stairs =
+ pick_stair_positions(&layouts[layout_idx], settings, stair_count, i as u64);
+ all_stair_sets.push(stairs);
+ }
}
- let is_elevator = settings.windows_enabled;
-
- // If syncing stairs, pick the same stair positions for all levels.
- let mut sync_stair_cells: Vec = Vec::new();
- if settings.sync_stairs_across_levels && !layouts.is_empty() {
- let base_settings = settings.clone();
- let base_layout = layouts[0].clone();
- sync_stair_cells =
- pick_stair_positions(&base_layout, &base_settings, stair_count, is_elevator);
+ // Assign stairs to levels.
+ if num_levels == 1 {
+ layouts[0].stairs.extend(all_stair_sets[0].clone());
+ } else {
+ for i in 0..num_gaps {
+ let stairs = &all_stair_sets[i];
+ // These stairs connect level i to i+1.
+ layouts[i].stairs.extend(stairs.clone());
+ layouts[i + 1].stairs.extend(stairs.clone());
+ }
}
- for (level_idx, layout) in layouts.iter_mut().enumerate() {
- if settings.sync_stairs_across_levels && !sync_stair_cells.is_empty() {
- // Use the synced stair positions.
- layout.stairs = sync_stair_cells
- .iter()
- .map(|stair| layout::Staircase {
- cell: stair.cell,
- size: random_stair_size(
- settings,
- seed::derive_seed(
- settings.seed,
- STAIR_MARKER_STREAM_BASE + level_idx as u64,
- ),
- ),
- is_elevator,
- })
- .collect();
- } else {
- // Generate independent stair positions for this level.
- layout.stairs = pick_stair_positions(layout, settings, stair_count, is_elevator);
+ // De-duplicate stairs at the same location on the same level (can happen if synced).
+ for layout in &mut layouts {
+ let mut seen = HashSet::new();
+ layout.stairs.retain(|s| seen.insert(s.cell));
+ }
+
+ // Ensure every stair is inside a room on its level.
+ for layout in &mut layouts {
+ let stairs_clone = layout.stairs.clone();
+ for stair in &stairs_clone {
+ ensure_stair_in_room(layout, stair);
}
}
layouts
}
+fn get_stair_count(settings: &UiSettings, gap_idx: usize) -> usize {
+ if settings.min_stairs_per_level == settings.max_stairs_per_level {
+ settings.min_stairs_per_level
+ } else {
+ let range_seed = seed::derive_seed(settings.seed, 0x2A_3B_4_u64 + gap_idx as u64);
+ (range_seed as usize % (settings.max_stairs_per_level - settings.min_stairs_per_level + 1))
+ + settings.min_stairs_per_level
+ }
+}
+
+// Ensure a staircase is fully contained within a room. If not, extend the nearest existing room or create a new one.
+fn ensure_stair_in_room(layout: &mut DungeonLayout, stair: &layout::Staircase) {
+ let stair_x = stair.cell.0;
+ let stair_y = stair.cell.1;
+ let stair_size = stair.size;
+
+ // Check if any room already contains the staircase.
+ for room in &layout.rooms {
+ if stair_x >= room.x
+ && stair_y >= room.y
+ && (stair_x + stair_size) <= (room.x + room.width)
+ && (stair_y + stair_size) <= (room.y + room.height)
+ {
+ return;
+ }
+ }
+
+ // Find the nearest room to the staircase.
+ let mut nearest_room_idx = None;
+ let mut min_dist = usize::MAX;
+
+ for (idx, room) in layout.rooms.iter().enumerate() {
+ let dist = room_to_stair_min_dist(room, stair);
+ if dist < min_dist {
+ min_dist = dist;
+ nearest_room_idx = Some(idx);
+ }
+ }
+
+ // If a room is nearby (within 5 cells), extend it.
+ if let Some(idx) = nearest_room_idx
+ && min_dist <= 5
+ {
+ let room = &mut layout.rooms[idx];
+ let new_x = room.x.min(stair_x);
+ let new_y = room.y.min(stair_y);
+ let new_right = (room.x + room.width).max(stair_x + stair_size);
+ let new_bottom = (room.y + room.height).max(stair_y + stair_size);
+
+ room.x = new_x;
+ room.y = new_y;
+ room.width = new_right - new_x;
+ room.height = new_bottom - new_y;
+ } else {
+ // Otherwise, create a new room for the staircase.
+ layout.rooms.push(layout::Room {
+ x: stair_x,
+ y: stair_y,
+ width: stair_size,
+ height: stair_size,
+ });
+ }
+}
+
+// Compute the minimum Manhattan distance between a room and a staircase.
+fn room_to_stair_min_dist(room: &layout::Room, stair: &layout::Staircase) -> usize {
+ let dx = if stair.cell.0 + stair.size <= room.x {
+ room.x - (stair.cell.0 + stair.size)
+ } else if stair.cell.0 >= room.x + room.width {
+ stair.cell.0 - (room.x + room.width)
+ } else {
+ 0
+ };
+
+ let dy = if stair.cell.1 + stair.size <= room.y {
+ room.y - (stair.cell.1 + stair.size)
+ } else if stair.cell.1 >= room.y + room.height {
+ stair.cell.1 - (room.y + room.height)
+ } else {
+ 0
+ };
+
+ dx + dy
+}
+
// Pick random cells within rooms that are valid for stair placement and return Staircase objects.
fn pick_stair_positions(
layout: &DungeonLayout,
settings: &UiSettings,
count: usize,
- is_elevator: bool,
+ seed_offset: u64,
) -> Vec {
if layout.rooms.is_empty() || count == 0 {
return Vec::new();
@@ -444,7 +545,10 @@ fn pick_stair_positions(
let mut shuffled = valid_cells.clone();
shuffle_with_seed(
&mut shuffled,
- settings.seed.wrapping_add(STAIR_MARKER_STREAM_BASE),
+ settings
+ .seed
+ .wrapping_add(STAIR_MARKER_STREAM_BASE)
+ .wrapping_add(seed_offset),
);
let max_size = settings.max_stair_width.max(settings.max_stair_height);
@@ -486,9 +590,11 @@ fn pick_stair_positions(
cell,
size: random_stair_size(
settings,
- seed::derive_seed(settings.seed, STAIR_MARKER_STREAM_BASE + i as u64),
+ seed::derive_seed(
+ settings.seed,
+ STAIR_MARKER_STREAM_BASE + i as u64 + seed_offset * 1000,
+ ),
),
- is_elevator,
})
.collect()
}
diff --git a/src/ui.rs b/src/ui.rs
index 489118f..fcd0e8a 100644
--- a/src/ui.rs
+++ b/src/ui.rs
@@ -221,7 +221,7 @@ impl Default for UiSettings {
max_stair_height: 3,
min_stairs_per_level: 1,
max_stairs_per_level: 1,
- sync_stairs_across_levels: false,
+ sync_stairs_across_levels: true,
active_tab: Tab::Generate,
}
}
@@ -267,47 +267,51 @@ pub fn draw_side_panel(
.default_width(280.0)
.show_separator_line(true)
.show(ctx, |ui| {
- ui.horizontal(|ui| {
- let is_generate = settings.active_tab == Tab::Generate;
- let is_layout = settings.active_tab == Tab::Layout;
+ egui::ScrollArea::vertical().show(ui, |ui| {
+ ui.horizontal(|ui| {
+ let is_generate = settings.active_tab == Tab::Generate;
+ let is_layout = settings.active_tab == Tab::Layout;
- if ui.selectable_label(is_generate, "Generate").clicked() {
- settings.active_tab = Tab::Generate;
- }
- if ui.selectable_label(is_layout, "Layout").clicked() {
- settings.active_tab = Tab::Layout;
- }
- if ui
- .selectable_label(settings.active_tab == Tab::StartAndEnd, "Start & End")
- .clicked()
- {
- settings.active_tab = Tab::StartAndEnd;
- }
- if ui
- .selectable_label(
- settings.active_tab == Tab::MonstersAndTraps,
- "Monsters/Traps",
- )
- .clicked()
- {
- settings.active_tab = Tab::MonstersAndTraps;
- }
- if ui
- .selectable_label(settings.active_tab == Tab::Add, "Add")
- .clicked()
- {
- settings.active_tab = Tab::Add;
+ if ui.selectable_label(is_generate, "Generate").clicked() {
+ settings.active_tab = Tab::Generate;
+ }
+ if ui.selectable_label(is_layout, "Layout").clicked() {
+ settings.active_tab = Tab::Layout;
+ }
+ if ui
+ .selectable_label(settings.active_tab == Tab::StartAndEnd, "Start & End")
+ .clicked()
+ {
+ settings.active_tab = Tab::StartAndEnd;
+ }
+ if ui
+ .selectable_label(
+ settings.active_tab == Tab::MonstersAndTraps,
+ "Monsters/Traps",
+ )
+ .clicked()
+ {
+ settings.active_tab = Tab::MonstersAndTraps;
+ }
+ if ui
+ .selectable_label(settings.active_tab == Tab::Add, "Add")
+ .clicked()
+ {
+ settings.active_tab = Tab::Add;
+ }
+ });
+ ui.separator();
+
+ match settings.active_tab {
+ Tab::Generate => {
+ draw_generate_tab(ui, settings, &mut result, export_in_progress)
+ }
+ Tab::Layout => draw_layout_tab(ui, settings, &mut result),
+ Tab::StartAndEnd => draw_start_and_end_tab(ui, settings, &mut result),
+ Tab::MonstersAndTraps => draw_monsters_and_traps_tab(ui, settings, &mut result),
+ Tab::Add => draw_add_tab(ui, settings, &mut result),
}
});
- ui.separator();
-
- match settings.active_tab {
- Tab::Generate => draw_generate_tab(ui, settings, &mut result, export_in_progress),
- Tab::Layout => draw_layout_tab(ui, settings, &mut result),
- Tab::StartAndEnd => draw_start_and_end_tab(ui, settings, &mut result),
- Tab::MonstersAndTraps => draw_monsters_and_traps_tab(ui, settings, &mut result),
- Tab::Add => draw_add_tab(ui, settings, &mut result),
- }
});
result
@@ -927,11 +931,6 @@ fn draw_layout_tab(ui: &mut egui::Ui, settings: &mut UiSettings, result: &mut Si
)
.changed();
});
-
- ui.add_space(8.0);
- result.settings_changed |= ui
- .checkbox(&mut settings.windows_enabled, "Enable Elevators")
- .changed();
}
// Render the Add tab controls.