made stairs removable

This commit is contained in:
grimsace
2026-04-27 12:21:13 -05:00
parent c4a8d606e1
commit 7a1456da90
2 changed files with 106 additions and 39 deletions
+89 -31
View File
@@ -72,10 +72,11 @@ impl Default for DungeonApp {
// Build the app with persisted settings and a generated layout. // Build the app with persisted settings and a generated layout.
fn default() -> Self { fn default() -> Self {
let settings = settings::load_settings().unwrap_or_default(); let settings = settings::load_settings().unwrap_or_default();
let levels = generate_all_levels(&settings); let layouts = generate_all_levels(&settings);
Self { let results = populate_stairs(layouts, &settings);
let mut app = Self {
settings, settings,
levels, levels: results.iter().map(|(l, _)| l.clone()).collect(),
suppressed_auto_door_edges: HashSet::new(), suppressed_auto_door_edges: HashSet::new(),
undo_stack: Vec::new(), undo_stack: Vec::new(),
redo_stack: Vec::new(), redo_stack: Vec::new(),
@@ -91,8 +92,14 @@ impl Default for DungeonApp {
hover_marker: None, hover_marker: None,
hover_level_idx: None, hover_level_idx: None,
hover_stair_idx: None, hover_stair_idx: None,
};
for (i, (_, modified)) in results.into_iter().enumerate() {
if modified {
app.refresh_level(i);
} }
} }
app
}
} }
impl Drop for DungeonApp { impl Drop for DungeonApp {
@@ -176,23 +183,18 @@ impl eframe::App for DungeonApp {
); );
let layout = populate_random_markers(layout, &self.settings); let layout = populate_random_markers(layout, &self.settings);
self.levels.push(layout); self.levels.push(layout);
let results = populate_stairs(self.levels.clone(), &self.settings);
self.levels = results.iter().map(|(l, _)| l.clone()).collect();
for (i, (_, modified)) in results.into_iter().enumerate() {
if modified {
self.refresh_level(i);
}
}
self.settings.active_level_index = self.levels.len() - 1; self.settings.active_level_index = self.levels.len() - 1;
} }
// Check for level deletion keybind when hovering over a level tab.
let level_delete_requested = ctx.input(|i| {
(i.key_pressed(egui::Key::Delete) || i.key_pressed(egui::Key::Backspace))
&& self.hover_level_idx.is_some()
});
if ctx.input(|i| i.key_pressed(egui::Key::Delete) || i.key_pressed(egui::Key::Backspace)) { if ctx.input(|i| i.key_pressed(egui::Key::Delete) || i.key_pressed(egui::Key::Backspace)) {
self.pending_delete = true; self.pending_delete = true;
} }
if level_delete_requested {
if let Some(level_idx) = self.hover_level_idx {
self.delete_level(level_idx);
}
}
if panel_result.export_clicked && self.export_rx.is_none() { if panel_result.export_clicked && self.export_rx.is_none() {
let target = match exporter::select_export_target(&self.settings) { let target = match exporter::select_export_target(&self.settings) {
Ok(target) => target, Ok(target) => target,
@@ -320,6 +322,18 @@ impl eframe::App for DungeonApp {
let canvas = response.rect.shrink(12.0); let canvas = response.rect.shrink(12.0);
let geometry = draw_grid(&painter, canvas, self.settings.cols, self.settings.rows); let geometry = draw_grid(&painter, canvas, self.settings.cols, self.settings.rows);
self.update_hover_targets(ctx, &geometry); self.update_hover_targets(ctx, &geometry);
// Check for level deletion keybind when hovering over a level tab.
let level_delete_requested = ctx.input(|i| {
(i.key_pressed(egui::Key::Delete) || i.key_pressed(egui::Key::Backspace))
&& self.hover_level_idx.is_some()
});
if level_delete_requested {
if let Some(level_idx) = self.hover_level_idx {
self.delete_level(level_idx);
}
}
let resizing = self.handle_resize(ctx, &response, &geometry); let resizing = self.handle_resize(ctx, &response, &geometry);
if !resizing { if !resizing {
self.handle_drag(ctx, &response, &geometry); self.handle_drag(ctx, &response, &geometry);
@@ -382,7 +396,6 @@ fn generate_all_levels(settings: &UiSettings) -> Vec<DungeonLayout> {
let layout = populate_random_markers(layout, settings); let layout = populate_random_markers(layout, settings);
levels.push(layout); levels.push(layout);
} }
let levels = populate_stairs(levels, settings);
levels levels
} }
@@ -504,7 +517,14 @@ impl DungeonApp {
fn regenerate_layout(&mut self) { fn regenerate_layout(&mut self) {
self.drag_state = None; self.drag_state = None;
self.suppressed_auto_door_edges.clear(); self.suppressed_auto_door_edges.clear();
self.levels = generate_all_levels(&self.settings); let layouts = generate_all_levels(&self.settings);
let results = populate_stairs(layouts, &self.settings);
self.levels = results.iter().map(|(l, _)| l.clone()).collect();
for (i, (_, modified)) in results.into_iter().enumerate() {
if modified {
self.refresh_level(i);
}
}
if self.settings.active_level_index >= self.levels.len() { if self.settings.active_level_index >= self.levels.len() {
self.settings.active_level_index = 0; self.settings.active_level_index = 0;
} }
@@ -801,6 +821,58 @@ impl DungeonApp {
} }
// Rebuild doors based on the current layout and settings. // Rebuild doors based on the current layout and settings.
fn refresh_level(&mut self, level_idx: usize) {
if level_idx >= self.levels.len() {
return;
}
// Reroute all corridors to match resized/moved rooms.
let layout = &mut self.levels[level_idx];
if !layout.packed_rooms {
let rooms = layout.rooms.clone();
for corridor in &mut layout.corridors {
let start = rooms[corridor.start_room_id].center_cell();
let end = rooms[corridor.end_room_id].center_cell();
let blocked =
blocked_room_cells(&rooms, &[corridor.start_room_id, corridor.end_room_id]);
if let Some(path) = shortest_path_cells(
start,
end,
self.settings.cols,
self.settings.rows,
&blocked,
) {
corridor.path = path;
}
}
}
// Rebuild doors.
let door_settings = door_settings_from_ui(&self.settings);
layout::apply_doors(
layout,
self.settings.seed,
door_settings,
self.settings.cols,
self.settings.rows,
);
layout.doors.retain(|door| {
!door_edges_for(door, self.settings.cols, self.settings.rows)
.iter()
.any(|edge| self.suppressed_auto_door_edges.contains(edge))
});
// Rebuild windows.
let window_settings = window_settings_from_ui(&self.settings);
layout::apply_windows(
layout,
self.settings.seed,
window_settings,
self.settings.cols,
self.settings.rows,
);
}
fn refresh_doors(&mut self) { fn refresh_doors(&mut self) {
if self.settings.active_level_index >= self.levels.len() { if self.settings.active_level_index >= self.levels.len() {
return; return;
@@ -1369,13 +1441,6 @@ impl DungeonApp {
stair.cell = new_cell; stair.cell = new_cell;
} }
} }
// Ensure room coverage for each stair moved.
let stairs_to_check = layout.stairs.clone();
for stair in &stairs_to_check {
if stair.cell == new_cell {
startend::ensure_stair_in_room(layout, stair);
}
}
} }
} else { } else {
// Move only the linked stairs (current level and adjacent levels if positions match). // Move only the linked stairs (current level and adjacent levels if positions match).
@@ -1397,13 +1462,6 @@ impl DungeonApp {
stair.cell = new_cell; stair.cell = new_cell;
} }
} }
// Ensure room coverage for each stair moved.
let stairs_to_check = layout.stairs.clone();
for stair in &stairs_to_check {
if stair.cell == new_cell {
startend::ensure_stair_in_room(layout, stair);
}
}
} }
} }
+17 -8
View File
@@ -354,12 +354,13 @@ fn pick_random_corridor_cell(
const STAIR_MARKER_STREAM_BASE: u64 = 21_000; const STAIR_MARKER_STREAM_BASE: u64 = 21_000;
// Populate stairs across all levels after markers are placed. // Populate stairs across all levels after markers are placed.
// Returns a vector of tuples: (DungeonLayout, room_modified)
pub fn populate_stairs( pub fn populate_stairs(
mut layouts: Vec<DungeonLayout>, mut layouts: Vec<DungeonLayout>,
settings: &UiSettings, settings: &UiSettings,
) -> Vec<DungeonLayout> { ) -> Vec<(DungeonLayout, bool)> {
if layouts.is_empty() { if layouts.is_empty() {
return layouts; return Vec::new();
} }
// Clear existing stairs. // Clear existing stairs.
@@ -368,7 +369,7 @@ pub fn populate_stairs(
} }
if settings.min_stairs_per_level == 0 && settings.max_stairs_per_level == 0 { if settings.min_stairs_per_level == 0 && settings.max_stairs_per_level == 0 {
return layouts; return layouts.into_iter().map(|l| (l, false)).collect();
} }
let num_levels = layouts.len(); let num_levels = layouts.len();
@@ -416,15 +417,20 @@ pub fn populate_stairs(
layout.stairs.retain(|s| seen.insert(s.cell)); layout.stairs.retain(|s| seen.insert(s.cell));
} }
let mut result = Vec::new();
// Ensure every stair is inside a room on its level. // Ensure every stair is inside a room on its level.
for layout in &mut layouts { for mut layout in layouts {
let mut modified = false;
let stairs_clone = layout.stairs.clone(); let stairs_clone = layout.stairs.clone();
for stair in &stairs_clone { for stair in &stairs_clone {
ensure_stair_in_room(layout, stair); if ensure_stair_in_room(&mut layout, stair) {
modified = true;
} }
} }
result.push((layout, modified));
}
layouts result
} }
fn get_stair_count(settings: &UiSettings, gap_idx: usize) -> usize { fn get_stair_count(settings: &UiSettings, gap_idx: usize) -> usize {
@@ -438,7 +444,8 @@ fn get_stair_count(settings: &UiSettings, gap_idx: usize) -> usize {
} }
// Ensure a staircase is fully contained within a room. If not, extend the nearest existing room or create a new one. // Ensure a staircase is fully contained within a room. If not, extend the nearest existing room or create a new one.
pub fn ensure_stair_in_room(layout: &mut DungeonLayout, stair: &layout::Staircase) { // Returns true if a room was modified or created.
pub fn ensure_stair_in_room(layout: &mut DungeonLayout, stair: &layout::Staircase) -> bool {
let stair_x = stair.cell.0; let stair_x = stair.cell.0;
let stair_y = stair.cell.1; let stair_y = stair.cell.1;
let stair_size = stair.size; let stair_size = stair.size;
@@ -450,7 +457,7 @@ pub fn ensure_stair_in_room(layout: &mut DungeonLayout, stair: &layout::Staircas
&& (stair_x + stair_size) <= (room.x + room.width) && (stair_x + stair_size) <= (room.x + room.width)
&& (stair_y + stair_size) <= (room.y + room.height) && (stair_y + stair_size) <= (room.y + room.height)
{ {
return; return false;
} }
} }
@@ -480,6 +487,7 @@ pub fn ensure_stair_in_room(layout: &mut DungeonLayout, stair: &layout::Staircas
room.y = new_y; room.y = new_y;
room.width = new_right - new_x; room.width = new_right - new_x;
room.height = new_bottom - new_y; room.height = new_bottom - new_y;
true
} else { } else {
// Otherwise, create a new room for the staircase. // Otherwise, create a new room for the staircase.
layout.rooms.push(layout::Room { layout.rooms.push(layout::Room {
@@ -488,6 +496,7 @@ pub fn ensure_stair_in_room(layout: &mut DungeonLayout, stair: &layout::Staircas
width: stair_size, width: stair_size,
height: stair_size, height: stair_size,
}); });
true
} }
} }