added initial level functionality

This commit is contained in:
grimsace
2026-04-23 10:30:49 -05:00
parent b1439d4872
commit fb81026ea6
2 changed files with 379 additions and 173 deletions
+319 -157
View File
@@ -33,7 +33,7 @@ enum ManualDoorKind {
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
struct AppSnapshot { struct AppSnapshot {
settings: UiSettings, settings: UiSettings,
layout: DungeonLayout, levels: Vec<DungeonLayout>,
suppressed_auto_door_edges: HashSet<((usize, usize), (usize, usize))>, suppressed_auto_door_edges: HashSet<((usize, usize), (usize, usize))>,
} }
@@ -50,7 +50,7 @@ fn main() -> eframe::Result<()> {
struct DungeonApp { struct DungeonApp {
settings: UiSettings, settings: UiSettings,
layout: DungeonLayout, levels: Vec<DungeonLayout>,
suppressed_auto_door_edges: HashSet<((usize, usize), (usize, usize))>, suppressed_auto_door_edges: HashSet<((usize, usize), (usize, usize))>,
undo_stack: Vec<AppSnapshot>, undo_stack: Vec<AppSnapshot>,
redo_stack: Vec<AppSnapshot>, redo_stack: Vec<AppSnapshot>,
@@ -70,26 +70,10 @@ 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 layout = layout::generate_layout( let levels = generate_all_levels(&settings);
settings.cols,
settings.rows,
settings.room_count,
settings.seed,
settings.min_room_size,
settings.max_room_size,
settings.square_rooms_only,
settings.min_corridor_width,
settings.max_corridor_width,
settings.corridor_randomness,
settings.dead_end_rooms_percent,
settings.pack_rooms_without_corridors,
door_settings_from_ui(&settings),
window_settings_from_ui(&settings),
);
let layout = populate_random_markers(layout, &settings);
Self { Self {
settings, settings,
layout, levels,
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(),
@@ -146,16 +130,20 @@ impl eframe::App for DungeonApp {
self.push_undo_snapshot(); self.push_undo_snapshot();
self.regenerate_layout(); self.regenerate_layout();
} else if panel_result.save_clicked { } else if panel_result.save_clicked {
let svg = exporter::build_svg(&self.layout, &self.settings); if self.settings.active_level_index < self.levels.len() {
if let Err(err) = saveandload::save_dungeon(&self.settings, &self.layout, svg) { let layout = &self.levels[self.settings.active_level_index];
let svg = exporter::build_svg(layout, &self.settings);
if let Err(err) = saveandload::save_dungeon(&self.settings, layout, svg) {
eprintln!("{err}"); eprintln!("{err}");
} }
}
} else if panel_result.load_clicked { } else if panel_result.load_clicked {
match saveandload::load_dungeon() { match saveandload::load_dungeon() {
Ok(data) => { Ok(data) => {
self.push_undo_snapshot(); self.push_undo_snapshot();
self.settings = data.settings; self.settings = data.settings;
self.layout = data.layout; self.levels = vec![data.layout];
self.settings.active_level_index = 0;
// Don't regenerate_layout() here to preserve the exact loaded state. // Don't regenerate_layout() here to preserve the exact loaded state.
} }
Err(err) => { Err(err) => {
@@ -187,7 +175,8 @@ impl eframe::App for DungeonApp {
} }
} }
let layout = self.layout.clone(); if self.settings.active_level_index < self.levels.len() {
let layout = self.levels[self.settings.active_level_index].clone();
let settings = self.settings.clone(); let settings = self.settings.clone();
let (tx, rx) = mpsc::channel(); let (tx, rx) = mpsc::channel();
thread::spawn(move || { thread::spawn(move || {
@@ -196,6 +185,7 @@ impl eframe::App for DungeonApp {
}); });
self.export_rx = Some(rx); self.export_rx = Some(rx);
} }
}
if let Some(rx) = &self.export_rx { if let Some(rx) = &self.export_rx {
match rx.try_recv() { match rx.try_recv() {
@@ -270,6 +260,19 @@ impl eframe::App for DungeonApp {
}); });
egui::CentralPanel::default().show(ctx, |ui| { egui::CentralPanel::default().show(ctx, |ui| {
ui.horizontal(|ui| {
for i in 0..self.levels.len() {
let label = format!("Level {}", i + 1);
if ui
.selectable_label(self.settings.active_level_index == i, label)
.clicked()
{
self.settings.active_level_index = i;
}
}
});
ui.separator();
let available = ui.available_size_before_wrap(); let available = ui.available_size_before_wrap();
let (response, painter) = ui.allocate_painter(available, egui::Sense::click_and_drag()); let (response, painter) = ui.allocate_painter(available, egui::Sense::click_and_drag());
let canvas = response.rect.shrink(12.0); let canvas = response.rect.shrink(12.0);
@@ -286,14 +289,17 @@ impl eframe::App for DungeonApp {
} }
self.pending_delete = false; self.pending_delete = false;
} }
if self.settings.active_level_index < self.levels.len() {
draw_layout( draw_layout(
&painter, &painter,
&geometry, &geometry,
&self.layout, &self.levels[self.settings.active_level_index],
self.settings.colorblind_mode, self.settings.colorblind_mode,
self.hover_text_idx, self.hover_text_idx,
self.hover_marker, self.hover_marker,
); );
}
self.draw_add_overlay(&painter, &geometry); self.draw_add_overlay(&painter, &geometry);
self.draw_resize_overlay(&painter, &geometry); self.draw_resize_overlay(&painter, &geometry);
self.draw_corridor_hover_overlay(&painter, &geometry); self.draw_corridor_hover_overlay(&painter, &geometry);
@@ -302,6 +308,40 @@ impl eframe::App for DungeonApp {
} }
} }
fn generate_all_levels(settings: &UiSettings) -> Vec<DungeonLayout> {
let level_count = if settings.min_levels == settings.max_levels {
settings.min_levels
} else {
let range_seed = seed::derive_seed(settings.seed, 0x1E_7E_1_u64);
(range_seed as usize % (settings.max_levels - settings.min_levels + 1))
+ settings.min_levels
};
let mut levels = Vec::with_capacity(level_count);
for i in 0..level_count {
let level_seed = seed::derive_seed(settings.seed, i as u64);
let layout = layout::generate_layout(
settings.cols,
settings.rows,
settings.room_count,
level_seed,
settings.min_room_size,
settings.max_room_size,
settings.square_rooms_only,
settings.min_corridor_width,
settings.max_corridor_width,
settings.corridor_randomness,
settings.dead_end_rooms_percent,
settings.pack_rooms_without_corridors,
door_settings_from_ui(settings),
window_settings_from_ui(settings),
);
let layout = populate_random_markers(layout, settings);
levels.push(layout);
}
levels
}
fn clamp_dependent_settings(settings: &mut UiSettings) { fn clamp_dependent_settings(settings: &mut UiSettings) {
if settings.min_room_size > settings.max_room_size { if settings.min_room_size > settings.max_room_size {
settings.max_room_size = settings.min_room_size; settings.max_room_size = settings.min_room_size;
@@ -333,6 +373,12 @@ fn clamp_dependent_settings(settings: &mut UiSettings) {
if settings.min_monsters_per_area > settings.max_monsters_per_area { if settings.min_monsters_per_area > settings.max_monsters_per_area {
settings.max_monsters_per_area = settings.min_monsters_per_area; settings.max_monsters_per_area = settings.min_monsters_per_area;
} }
if settings.min_levels == 0 {
settings.min_levels = 1;
}
if settings.min_levels > settings.max_levels {
settings.max_levels = settings.min_levels;
}
} }
impl DungeonApp { impl DungeonApp {
@@ -355,7 +401,7 @@ impl DungeonApp {
fn snapshot(&self) -> AppSnapshot { fn snapshot(&self) -> AppSnapshot {
AppSnapshot { AppSnapshot {
settings: self.settings.clone(), settings: self.settings.clone(),
layout: self.layout.clone(), levels: self.levels.clone(),
suppressed_auto_door_edges: self.suppressed_auto_door_edges.clone(), suppressed_auto_door_edges: self.suppressed_auto_door_edges.clone(),
} }
} }
@@ -374,7 +420,7 @@ impl DungeonApp {
fn restore_snapshot(&mut self, snapshot: AppSnapshot) { fn restore_snapshot(&mut self, snapshot: AppSnapshot) {
self.settings = snapshot.settings; self.settings = snapshot.settings;
self.layout = snapshot.layout; self.levels = snapshot.levels;
self.suppressed_auto_door_edges = snapshot.suppressed_auto_door_edges; self.suppressed_auto_door_edges = snapshot.suppressed_auto_door_edges;
self.reset_transient_state(); self.reset_transient_state();
} }
@@ -397,7 +443,10 @@ impl DungeonApp {
// Clear the current layout and reset transient interaction state. // Clear the current layout and reset transient interaction state.
fn clear_layout(&mut self) { fn clear_layout(&mut self) {
self.layout = DungeonLayout { if self.settings.active_level_index >= self.levels.len() {
return;
}
self.levels[self.settings.active_level_index] = DungeonLayout {
packed_rooms: self.settings.pack_rooms_without_corridors, packed_rooms: self.settings.pack_rooms_without_corridors,
..DungeonLayout::default() ..DungeonLayout::default()
}; };
@@ -409,23 +458,10 @@ 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();
let layout = layout::generate_layout( self.levels = generate_all_levels(&self.settings);
self.settings.cols, if self.settings.active_level_index >= self.levels.len() {
self.settings.rows, self.settings.active_level_index = 0;
self.settings.room_count, }
self.settings.seed,
self.settings.min_room_size,
self.settings.max_room_size,
self.settings.square_rooms_only,
self.settings.min_corridor_width,
self.settings.max_corridor_width,
self.settings.corridor_randomness,
self.settings.dead_end_rooms_percent,
self.settings.pack_rooms_without_corridors,
door_settings_from_ui(&self.settings),
window_settings_from_ui(&self.settings),
);
self.layout = populate_random_markers(layout, &self.settings);
} }
// Handle dragging rooms or corridors with the primary mouse button. // Handle dragging rooms or corridors with the primary mouse button.
@@ -499,7 +535,12 @@ impl DungeonApp {
) -> Option<(usize, f32, f32)> { ) -> Option<(usize, f32, f32)> {
let (grid_x, grid_y) = pointer_to_grid(pointer_pos, geometry)?; let (grid_x, grid_y) = pointer_to_grid(pointer_pos, geometry)?;
for (room_idx, room) in self.layout.rooms.iter().enumerate().rev() { if self.settings.active_level_index >= self.levels.len() {
return None;
}
let layout = &self.levels[self.settings.active_level_index];
for (room_idx, room) in layout.rooms.iter().enumerate().rev() {
let room_x = room.x as f32; let room_x = room.x as f32;
let room_y = room.y as f32; let room_y = room.y as f32;
let room_right = room_x + room.width as f32; let room_right = room_x + room.width as f32;
@@ -524,13 +565,17 @@ impl DungeonApp {
return; return;
}; };
if drag.room_idx >= self.layout.rooms.len() { if self.settings.active_level_index >= self.levels.len() {
return;
}
if drag.room_idx >= self.levels[self.settings.active_level_index].rooms.len() {
self.drag_state = None; self.drag_state = None;
return; return;
} }
let (width, height, old_x, old_y) = { let (width, height, old_x, old_y) = {
let room = &self.layout.rooms[drag.room_idx]; let room = &self.levels[self.settings.active_level_index].rooms[drag.room_idx];
(room.width, room.height, room.x, room.y) (room.width, room.height, room.x, room.y)
}; };
let max_x = self.settings.cols.saturating_sub(width); let max_x = self.settings.cols.saturating_sub(width);
@@ -545,8 +590,8 @@ impl DungeonApp {
return; return;
} }
self.layout.rooms[drag.room_idx].x = new_x; self.levels[self.settings.active_level_index].rooms[drag.room_idx].x = new_x;
self.layout.rooms[drag.room_idx].y = new_y; self.levels[self.settings.active_level_index].rooms[drag.room_idx].y = new_y;
self.reroute_corridors_for_room(drag.room_idx); self.reroute_corridors_for_room(drag.room_idx);
self.refresh_doors(); self.refresh_doors();
} }
@@ -561,7 +606,15 @@ impl DungeonApp {
return; return;
}; };
if drag.text_idx >= self.layout.text_labels.len() { if self.settings.active_level_index >= self.levels.len() {
return;
}
if drag.text_idx
>= self.levels[self.settings.active_level_index]
.text_labels
.len()
{
self.drag_state = None; self.drag_state = None;
return; return;
} }
@@ -571,24 +624,29 @@ impl DungeonApp {
(grid_y.floor().max(0.0) as usize).min(self.settings.rows.saturating_sub(1)), (grid_y.floor().max(0.0) as usize).min(self.settings.rows.saturating_sub(1)),
); );
self.layout.text_labels[drag.text_idx].cell = target; self.levels[self.settings.active_level_index].text_labels[drag.text_idx].cell = target;
} }
// Recalculate corridor paths connected to a moved room. // Recalculate corridor paths connected to a moved room.
fn reroute_corridors_for_room(&mut self, room_idx: usize) { fn reroute_corridors_for_room(&mut self, room_idx: usize) {
if self.layout.packed_rooms { if self.settings.active_level_index >= self.levels.len() {
return;
}
let layout = &mut self.levels[self.settings.active_level_index];
if layout.packed_rooms {
return; return;
} }
for corridor in &mut self.layout.corridors { for corridor in &mut layout.corridors {
if corridor.start_room_id != room_idx && corridor.end_room_id != room_idx { if corridor.start_room_id != room_idx && corridor.end_room_id != room_idx {
continue; continue;
} }
let start = self.layout.rooms[corridor.start_room_id].center_cell(); let start = layout.rooms[corridor.start_room_id].center_cell();
let end = self.layout.rooms[corridor.end_room_id].center_cell(); let end = layout.rooms[corridor.end_room_id].center_cell();
let blocked = blocked_room_cells( let blocked = blocked_room_cells(
&self.layout.rooms, &layout.rooms,
&[corridor.start_room_id, corridor.end_room_id], &[corridor.start_room_id, corridor.end_room_id],
); );
if let Some(path) = if let Some(path) =
@@ -607,15 +665,19 @@ impl DungeonApp {
) -> Option<CorridorDragState> { ) -> Option<CorridorDragState> {
let (grid_x, grid_y) = pointer_to_grid(pointer_pos, geometry)?; let (grid_x, grid_y) = pointer_to_grid(pointer_pos, geometry)?;
let clicked = (grid_x.floor() as usize, grid_y.floor() as usize); let clicked = (grid_x.floor() as usize, grid_y.floor() as usize);
let corridor_index = self if self.settings.active_level_index >= self.levels.len() {
.layout return None;
}
let layout = &self.levels[self.settings.active_level_index];
let corridor_index = layout
.corridors .corridors
.iter() .iter()
.position(|corridor| corridor.path.contains(&clicked))?; .position(|corridor| corridor.path.contains(&clicked))?;
Some(CorridorDragState { Some(CorridorDragState {
corridor_index, corridor_index,
original_path: self.layout.corridors[corridor_index].path.clone(), original_path: layout.corridors[corridor_index].path.clone(),
original_cell: clicked, original_cell: clicked,
}) })
} }
@@ -631,11 +693,16 @@ impl DungeonApp {
return; return;
}; };
if drag.corridor_index >= self.layout.corridors.len() { if self.settings.active_level_index >= self.levels.len() {
return;
}
let layout = &mut self.levels[self.settings.active_level_index];
if drag.corridor_index >= layout.corridors.len() {
self.drag_state = None; self.drag_state = None;
return; return;
} }
let corridor = &self.layout.corridors[drag.corridor_index]; let corridor = &layout.corridors[drag.corridor_index];
let target = ( let target = (
(grid_x.floor().max(0.0) as usize).min(self.settings.cols.saturating_sub(1)), (grid_x.floor().max(0.0) as usize).min(self.settings.cols.saturating_sub(1)),
@@ -649,33 +716,38 @@ impl DungeonApp {
self.settings.cols, self.settings.cols,
self.settings.rows, self.settings.rows,
&blocked_room_cells( &blocked_room_cells(
&self.layout.rooms, &layout.rooms,
&[corridor.start_room_id, corridor.end_room_id], &[corridor.start_room_id, corridor.end_room_id],
), ),
) else { ) else {
return; return;
}; };
self.layout.corridors[drag.corridor_index].path = new_path; layout.corridors[drag.corridor_index].path = new_path;
self.refresh_doors(); self.refresh_doors();
} }
// Rebuild doors based on the current layout and settings. // Rebuild doors based on the current layout and settings.
fn refresh_doors(&mut self) { fn refresh_doors(&mut self) {
if self.settings.active_level_index >= self.levels.len() {
return;
}
let layout = &mut self.levels[self.settings.active_level_index];
layout::apply_doors( layout::apply_doors(
&mut self.layout, layout,
self.settings.seed, self.settings.seed,
door_settings_from_ui(&self.settings), door_settings_from_ui(&self.settings),
self.settings.cols, self.settings.cols,
self.settings.rows, self.settings.rows,
); );
self.layout.doors.retain(|door| { layout.doors.retain(|door| {
!door_edges_for(door, self.settings.cols, self.settings.rows) !door_edges_for(door, self.settings.cols, self.settings.rows)
.iter() .iter()
.any(|edge| self.suppressed_auto_door_edges.contains(edge)) .any(|edge| self.suppressed_auto_door_edges.contains(edge))
}); });
layout::apply_windows( layout::apply_windows(
&mut self.layout, layout,
self.settings.seed, self.settings.seed,
window_settings_from_ui(&self.settings), window_settings_from_ui(&self.settings),
self.settings.cols, self.settings.cols,
@@ -794,6 +866,11 @@ impl DungeonApp {
// Add a room anchored to the given grid cell if space allows. // Add a room anchored to the given grid cell if space allows.
fn add_room_at_cell(&mut self, cell: (usize, usize)) { fn add_room_at_cell(&mut self, cell: (usize, usize)) {
self.push_undo_snapshot(); self.push_undo_snapshot();
if self.settings.active_level_index >= self.levels.len() {
return;
}
let layout = &mut self.levels[self.settings.active_level_index];
let width = self.settings.min_room_size.max(1); let width = self.settings.min_room_size.max(1);
let height = self.settings.min_room_size.max(1); let height = self.settings.min_room_size.max(1);
if cell.0 >= self.settings.cols || cell.1 >= self.settings.rows { if cell.0 >= self.settings.cols || cell.1 >= self.settings.rows {
@@ -814,8 +891,7 @@ impl DungeonApp {
height, height,
}; };
if self if layout
.layout
.rooms .rooms
.iter() .iter()
.any(|room| rooms_overlap(&new_room, room)) .any(|room| rooms_overlap(&new_room, room))
@@ -823,24 +899,29 @@ impl DungeonApp {
return; return;
} }
self.layout.rooms.push(new_room); layout.rooms.push(new_room);
self.refresh_doors(); self.refresh_doors();
} }
// Add a corridor between two room cells using a shortest path. // Add a corridor between two room cells using a shortest path.
fn add_corridor_between(&mut self, start: (usize, usize), end: (usize, usize)) { fn add_corridor_between(&mut self, start: (usize, usize), end: (usize, usize)) {
self.push_undo_snapshot(); self.push_undo_snapshot();
if self.settings.active_level_index >= self.levels.len() {
return;
}
let layout = &mut self.levels[self.settings.active_level_index];
if start == end { if start == end {
return; return;
} }
let start_room = room_index_at_cell(&self.layout.rooms, start); let start_room = room_index_at_cell(&layout.rooms, start);
let end_room = room_index_at_cell(&self.layout.rooms, end); let end_room = room_index_at_cell(&layout.rooms, end);
let (Some(start_room), Some(end_room)) = (start_room, end_room) else { let (Some(start_room), Some(end_room)) = (start_room, end_room) else {
return; return;
}; };
let blocked = blocked_room_cells(&self.layout.rooms, &[start_room, end_room]); let blocked = blocked_room_cells(&layout.rooms, &[start_room, end_room]);
let Some(path) = let Some(path) =
shortest_path_cells(start, end, self.settings.cols, self.settings.rows, &blocked) shortest_path_cells(start, end, self.settings.cols, self.settings.rows, &blocked)
@@ -848,8 +929,7 @@ impl DungeonApp {
return; return;
}; };
let next_id = self let next_id = layout
.layout
.corridors .corridors
.iter() .iter()
.map(|c| c.id) .map(|c| c.id)
@@ -858,7 +938,7 @@ impl DungeonApp {
.wrapping_add(1); .wrapping_add(1);
let width = self.settings.min_corridor_width.max(1); let width = self.settings.min_corridor_width.max(1);
self.layout.corridors.push(layout::Corridor { layout.corridors.push(layout::Corridor {
id: next_id, id: next_id,
start_room_id: start_room, start_room_id: start_room,
end_room_id: end_room, end_room_id: end_room,
@@ -869,6 +949,12 @@ impl DungeonApp {
} }
fn add_text_at_cell(&mut self, cell: (usize, usize)) { fn add_text_at_cell(&mut self, cell: (usize, usize)) {
self.push_undo_snapshot();
if self.settings.active_level_index >= self.levels.len() {
return;
}
let layout = &mut self.levels[self.settings.active_level_index];
if cell.0 >= self.settings.cols || cell.1 >= self.settings.rows { if cell.0 >= self.settings.cols || cell.1 >= self.settings.rows {
return; return;
} }
@@ -878,9 +964,7 @@ impl DungeonApp {
return; return;
} }
self.push_undo_snapshot(); if let Some(existing) = layout
if let Some(existing) = self
.layout
.text_labels .text_labels
.iter_mut() .iter_mut()
.find(|label| label.cell == cell) .find(|label| label.cell == cell)
@@ -889,7 +973,7 @@ impl DungeonApp {
return; return;
} }
self.layout.text_labels.push(layout::TextLabel { layout.text_labels.push(layout::TextLabel {
cell, cell,
text, text,
font_size: 18, font_size: 18,
@@ -897,6 +981,12 @@ impl DungeonApp {
} }
fn add_marker_at_cell(&mut self, cell: (usize, usize), kind: MarkerKind) { fn add_marker_at_cell(&mut self, cell: (usize, usize), kind: MarkerKind) {
self.push_undo_snapshot();
if self.settings.active_level_index >= self.levels.len() {
return;
}
let layout = &mut self.levels[self.settings.active_level_index];
if cell.0 >= self.settings.cols || cell.1 >= self.settings.rows { if cell.0 >= self.settings.cols || cell.1 >= self.settings.rows {
return; return;
} }
@@ -910,12 +1000,11 @@ impl DungeonApp {
let origin = centered_marker_origin(cell, size, self.settings.cols, self.settings.rows); let origin = centered_marker_origin(cell, size, self.settings.cols, self.settings.rows);
let marker = layout::AreaMarker { cell: origin, size }; let marker = layout::AreaMarker { cell: origin, size };
self.push_undo_snapshot();
match kind { match kind {
MarkerKind::Start => self.layout.start_markers.push(marker), MarkerKind::Start => layout.start_markers.push(marker),
MarkerKind::End => self.layout.end_markers.push(marker), MarkerKind::End => layout.end_markers.push(marker),
MarkerKind::Trap => self.layout.trap_markers.push(marker), MarkerKind::Trap => layout.trap_markers.push(marker),
MarkerKind::Monster => self.layout.monster_markers.push(marker), MarkerKind::Monster => layout.monster_markers.push(marker),
} }
} }
@@ -925,9 +1014,14 @@ impl DungeonApp {
self.clear_hover_targets(); self.clear_hover_targets();
return; return;
}; };
if self.settings.active_level_index >= self.levels.len() {
return;
}
let layout = &self.levels[self.settings.active_level_index];
if let Some(edge) = self.door_edge_at_pointer(pointer_pos, geometry) { if let Some(edge) = self.door_edge_at_pointer(pointer_pos, geometry) {
let target = normalized_edge(edge.0, edge.1); let target = normalized_edge(edge.0, edge.1);
self.hover_door_idx = self.layout.doors.iter().position(|d| { self.hover_door_idx = layout.doors.iter().position(|d| {
door_edges_for(d, self.settings.cols, self.settings.rows) door_edges_for(d, self.settings.cols, self.settings.rows)
.iter() .iter()
.any(|e| normalized_edge(e.0, e.1) == target) .any(|e| normalized_edge(e.0, e.1) == target)
@@ -980,7 +1074,12 @@ impl DungeonApp {
fn text_at_pointer(&self, pointer_pos: egui::Pos2, geometry: &GridGeometry) -> Option<usize> { fn text_at_pointer(&self, pointer_pos: egui::Pos2, geometry: &GridGeometry) -> Option<usize> {
let (grid_x, grid_y) = pointer_to_grid(pointer_pos, geometry)?; let (grid_x, grid_y) = pointer_to_grid(pointer_pos, geometry)?;
let clicked = (grid_x.floor() as usize, grid_y.floor() as usize); let clicked = (grid_x.floor() as usize, grid_y.floor() as usize);
self.layout if self.settings.active_level_index >= self.levels.len() {
return None;
}
let layout = &self.levels[self.settings.active_level_index];
layout
.text_labels .text_labels
.iter() .iter()
.position(|label| label.cell == clicked) .position(|label| label.cell == clicked)
@@ -993,14 +1092,19 @@ impl DungeonApp {
) -> Option<HoverMarker> { ) -> Option<HoverMarker> {
let (grid_x, grid_y) = pointer_to_grid(pointer_pos, geometry)?; let (grid_x, grid_y) = pointer_to_grid(pointer_pos, geometry)?;
let clicked = (grid_x.floor() as usize, grid_y.floor() as usize); let clicked = (grid_x.floor() as usize, grid_y.floor() as usize);
self.layout if self.settings.active_level_index >= self.levels.len() {
return None;
}
let layout = &self.levels[self.settings.active_level_index];
layout
.start_markers .start_markers
.iter() .iter()
.enumerate() .enumerate()
.find(|(_, marker)| marker_contains_cell(Some(marker), clicked)) .find(|(_, marker)| marker_contains_cell(Some(marker), clicked))
.map(|(idx, _)| HoverMarker::Start(idx)) .map(|(idx, _)| HoverMarker::Start(idx))
.or_else(|| { .or_else(|| {
self.layout layout
.end_markers .end_markers
.iter() .iter()
.enumerate() .enumerate()
@@ -1008,7 +1112,7 @@ impl DungeonApp {
.map(|(idx, _)| HoverMarker::End(idx)) .map(|(idx, _)| HoverMarker::End(idx))
}) })
.or_else(|| { .or_else(|| {
self.layout layout
.trap_markers .trap_markers
.iter() .iter()
.enumerate() .enumerate()
@@ -1016,7 +1120,7 @@ impl DungeonApp {
.map(|(idx, _)| HoverMarker::Trap(idx)) .map(|(idx, _)| HoverMarker::Trap(idx))
}) })
.or_else(|| { .or_else(|| {
self.layout layout
.monster_markers .monster_markers
.iter() .iter()
.enumerate() .enumerate()
@@ -1031,9 +1135,14 @@ impl DungeonApp {
geometry: &GridGeometry, geometry: &GridGeometry,
) -> Option<MarkerDragState> { ) -> Option<MarkerDragState> {
let (grid_x, grid_y) = pointer_to_grid(pointer_pos, geometry)?; let (grid_x, grid_y) = pointer_to_grid(pointer_pos, geometry)?;
if self.settings.active_level_index >= self.levels.len() {
return None;
}
let layout = &self.levels[self.settings.active_level_index];
match self.marker_at_pointer(pointer_pos, geometry)? { match self.marker_at_pointer(pointer_pos, geometry)? {
HoverMarker::Start(marker_idx) => { HoverMarker::Start(marker_idx) => {
let marker = self.layout.start_markers.get(marker_idx)?; let marker = layout.start_markers.get(marker_idx)?;
Some(MarkerDragState { Some(MarkerDragState {
kind: MarkerKind::Start, kind: MarkerKind::Start,
marker_idx, marker_idx,
@@ -1042,7 +1151,7 @@ impl DungeonApp {
}) })
} }
HoverMarker::End(marker_idx) => { HoverMarker::End(marker_idx) => {
let marker = self.layout.end_markers.get(marker_idx)?; let marker = layout.end_markers.get(marker_idx)?;
Some(MarkerDragState { Some(MarkerDragState {
kind: MarkerKind::End, kind: MarkerKind::End,
marker_idx, marker_idx,
@@ -1051,7 +1160,7 @@ impl DungeonApp {
}) })
} }
HoverMarker::Trap(marker_idx) => { HoverMarker::Trap(marker_idx) => {
let marker = self.layout.trap_markers.get(marker_idx)?; let marker = layout.trap_markers.get(marker_idx)?;
Some(MarkerDragState { Some(MarkerDragState {
kind: MarkerKind::Trap, kind: MarkerKind::Trap,
marker_idx, marker_idx,
@@ -1060,7 +1169,7 @@ impl DungeonApp {
}) })
} }
HoverMarker::Monster(marker_idx) => { HoverMarker::Monster(marker_idx) => {
let marker = self.layout.monster_markers.get(marker_idx)?; let marker = layout.monster_markers.get(marker_idx)?;
Some(MarkerDragState { Some(MarkerDragState {
kind: MarkerKind::Monster, kind: MarkerKind::Monster,
marker_idx, marker_idx,
@@ -1081,11 +1190,16 @@ impl DungeonApp {
return; return;
}; };
if self.settings.active_level_index >= self.levels.len() {
return;
}
let layout = &mut self.levels[self.settings.active_level_index];
let markers = match drag.kind { let markers = match drag.kind {
MarkerKind::Start => &mut self.layout.start_markers, MarkerKind::Start => &mut layout.start_markers,
MarkerKind::End => &mut self.layout.end_markers, MarkerKind::End => &mut layout.end_markers,
MarkerKind::Trap => &mut self.layout.trap_markers, MarkerKind::Trap => &mut layout.trap_markers,
MarkerKind::Monster => &mut self.layout.monster_markers, MarkerKind::Monster => &mut layout.monster_markers,
}; };
let Some(marker) = markers.get_mut(drag.marker_idx) else { let Some(marker) = markers.get_mut(drag.marker_idx) else {
self.drag_state = None; self.drag_state = None;
@@ -1152,7 +1266,12 @@ impl DungeonApp {
} }
fn start_text_resize(&self, text_idx: usize) -> Option<TextResizeState> { fn start_text_resize(&self, text_idx: usize) -> Option<TextResizeState> {
let label = self.layout.text_labels.get(text_idx)?; if self.settings.active_level_index >= self.levels.len() {
return None;
}
let layout = &self.levels[self.settings.active_level_index];
let label = layout.text_labels.get(text_idx)?;
Some(TextResizeState { Some(TextResizeState {
text_idx, text_idx,
origin_cell: label.cell, origin_cell: label.cell,
@@ -1176,14 +1295,13 @@ impl DungeonApp {
grid_y.floor().max(0.0) as usize, grid_y.floor().max(0.0) as usize,
); );
for (room_idx, room) in self.layout.rooms.iter().enumerate().rev() { if self.settings.active_level_index >= self.levels.len() {
if resize_hit_cells( return None;
room, }
room_idx, let layout = &self.levels[self.settings.active_level_index];
&self.layout.rooms,
geometry.cols, for (room_idx, room) in layout.rooms.iter().enumerate().rev() {
geometry.rows, if resize_hit_cells(room, room_idx, &layout.rooms, geometry.cols, geometry.rows)
)
.contains(&clicked) .contains(&clicked)
{ {
return Some(room_idx); return Some(room_idx);
@@ -1200,7 +1318,12 @@ impl DungeonApp {
pointer_pos: egui::Pos2, pointer_pos: egui::Pos2,
geometry: &GridGeometry, geometry: &GridGeometry,
) -> Option<RoomResizeState> { ) -> Option<RoomResizeState> {
let original_room = self.layout.rooms.get(room_idx)?.clone(); if self.settings.active_level_index >= self.levels.len() {
return None;
}
let layout = &self.levels[self.settings.active_level_index];
let original_room = layout.rooms.get(room_idx)?.clone();
let (grid_x, grid_y) = pointer_to_grid(pointer_pos, geometry)?; let (grid_x, grid_y) = pointer_to_grid(pointer_pos, geometry)?;
let center_x = original_room.x as f32 + original_room.width as f32 * 0.5; let center_x = original_room.x as f32 + original_room.width as f32 * 0.5;
let center_y = original_room.y as f32 + original_room.height as f32 * 0.5; let center_y = original_room.y as f32 + original_room.height as f32 * 0.5;
@@ -1223,6 +1346,11 @@ impl DungeonApp {
// Resize only the corner chosen at drag start while keeping the opposite corner fixed. // Resize only the corner chosen at drag start while keeping the opposite corner fixed.
fn resize_room(&mut self, state: &RoomResizeState, target_cell: (usize, usize)) { fn resize_room(&mut self, state: &RoomResizeState, target_cell: (usize, usize)) {
if self.settings.active_level_index >= self.levels.len() {
return;
}
let layout = &mut self.levels[self.settings.active_level_index];
let cols = self.settings.cols.max(1); let cols = self.settings.cols.max(1);
let rows = self.settings.rows.max(1); let rows = self.settings.rows.max(1);
@@ -1256,8 +1384,7 @@ impl DungeonApp {
return; return;
} }
if self if layout
.layout
.rooms .rooms
.iter() .iter()
.enumerate() .enumerate()
@@ -1266,7 +1393,7 @@ impl DungeonApp {
return; return;
} }
if let Some(room) = self.layout.rooms.get_mut(state.room_idx) { if let Some(room) = layout.rooms.get_mut(state.room_idx) {
room.x = new_room.x; room.x = new_room.x;
room.y = new_room.y; room.y = new_room.y;
room.width = new_room.width; room.width = new_room.width;
@@ -1277,7 +1404,12 @@ impl DungeonApp {
} }
fn resize_text(&mut self, state: &TextResizeState, target_cell: (usize, usize)) { fn resize_text(&mut self, state: &TextResizeState, target_cell: (usize, usize)) {
let Some(label) = self.layout.text_labels.get_mut(state.text_idx) else { if self.settings.active_level_index >= self.levels.len() {
return;
}
let layout = &mut self.levels[self.settings.active_level_index];
let Some(label) = layout.text_labels.get_mut(state.text_idx) else {
self.resize_state = None; self.resize_state = None;
return; return;
}; };
@@ -1291,6 +1423,11 @@ impl DungeonApp {
// Draw dashed overlays for room resizing interactions. // Draw dashed overlays for room resizing interactions.
fn draw_resize_overlay(&self, painter: &egui::Painter, geometry: &GridGeometry) { fn draw_resize_overlay(&self, painter: &egui::Painter, geometry: &GridGeometry) {
if self.settings.active_level_index >= self.levels.len() {
return;
}
let layout = &self.levels[self.settings.active_level_index];
if let Some(text_idx) = self if let Some(text_idx) = self
.resize_state .resize_state
.as_ref() .as_ref()
@@ -1300,7 +1437,7 @@ impl DungeonApp {
}) })
.or(self.hover_text_idx) .or(self.hover_text_idx)
{ {
if let Some(label) = self.layout.text_labels.get(text_idx) { if let Some(label) = layout.text_labels.get(text_idx) {
let rect = text_label_rect(geometry, label).expand(3.0); let rect = text_label_rect(geometry, label).expand(3.0);
painter.rect_stroke( painter.rect_stroke(
rect, rect,
@@ -1323,7 +1460,7 @@ impl DungeonApp {
let Some(room_idx) = room_idx else { let Some(room_idx) = room_idx else {
return; return;
}; };
let Some(room) = self.layout.rooms.get(room_idx) else { let Some(room) = layout.rooms.get(room_idx) else {
return; return;
}; };
let color = Color32::from_gray(170); let color = Color32::from_gray(170);
@@ -1369,7 +1506,12 @@ impl DungeonApp {
let Some(idx) = self.hover_corridor_idx else { let Some(idx) = self.hover_corridor_idx else {
return; return;
}; };
let Some(corridor) = self.layout.corridors.get(idx) else { if self.settings.active_level_index >= self.levels.len() {
return;
}
let layout = &self.levels[self.settings.active_level_index];
let Some(corridor) = layout.corridors.get(idx) else {
return; return;
}; };
@@ -1387,7 +1529,12 @@ impl DungeonApp {
let Some(idx) = self.hover_door_idx else { let Some(idx) = self.hover_door_idx else {
return; return;
}; };
let Some(door) = self.layout.doors.get(idx) else { if self.settings.active_level_index >= self.levels.len() {
return;
}
let layout = &self.levels[self.settings.active_level_index];
let Some(door) = layout.doors.get(idx) else {
return; return;
}; };
let color = Color32::from_gray(200); let color = Color32::from_gray(200);
@@ -1405,35 +1552,41 @@ impl DungeonApp {
// Delete the door, room, or corridor at the pointer position. // Delete the door, room, or corridor at the pointer position.
fn delete_at_pointer(&mut self, pointer_pos: egui::Pos2, geometry: &GridGeometry) { fn delete_at_pointer(&mut self, pointer_pos: egui::Pos2, geometry: &GridGeometry) {
if self.settings.active_level_index >= self.levels.len() {
return;
}
if let Some(idx) = self.text_at_pointer(pointer_pos, geometry) { if let Some(idx) = self.text_at_pointer(pointer_pos, geometry) {
self.push_undo_snapshot(); self.push_undo_snapshot();
if idx < self.layout.text_labels.len() { let layout = &mut self.levels[self.settings.active_level_index];
self.layout.text_labels.remove(idx); if idx < layout.text_labels.len() {
layout.text_labels.remove(idx);
} }
return; return;
} }
if let Some(marker) = self.marker_at_pointer(pointer_pos, geometry) { if let Some(marker) = self.marker_at_pointer(pointer_pos, geometry) {
self.push_undo_snapshot(); self.push_undo_snapshot();
let layout = &mut self.levels[self.settings.active_level_index];
match marker { match marker {
HoverMarker::Start(idx) => { HoverMarker::Start(idx) => {
if idx < self.layout.start_markers.len() { if idx < layout.start_markers.len() {
self.layout.start_markers.remove(idx); layout.start_markers.remove(idx);
} }
} }
HoverMarker::End(idx) => { HoverMarker::End(idx) => {
if idx < self.layout.end_markers.len() { if idx < layout.end_markers.len() {
self.layout.end_markers.remove(idx); layout.end_markers.remove(idx);
} }
} }
HoverMarker::Trap(idx) => { HoverMarker::Trap(idx) => {
if idx < self.layout.trap_markers.len() { if idx < layout.trap_markers.len() {
self.layout.trap_markers.remove(idx); layout.trap_markers.remove(idx);
} }
} }
HoverMarker::Monster(idx) => { HoverMarker::Monster(idx) => {
if idx < self.layout.monster_markers.len() { if idx < layout.monster_markers.len() {
self.layout.monster_markers.remove(idx); layout.monster_markers.remove(idx);
} }
} }
} }
@@ -1442,25 +1595,25 @@ impl DungeonApp {
if let Some(edge) = self.door_edge_at_pointer(pointer_pos, geometry) { if let Some(edge) = self.door_edge_at_pointer(pointer_pos, geometry) {
let target = normalized_edge(edge.0, edge.1); let target = normalized_edge(edge.0, edge.1);
if let Some(idx) = self.layout.doors.iter().position(|d| { let active_index = self.settings.active_level_index;
if let Some(idx) = self.levels[active_index].doors.iter().position(|d| {
door_edges_for(d, self.settings.cols, self.settings.rows) door_edges_for(d, self.settings.cols, self.settings.rows)
.iter() .iter()
.any(|e| normalized_edge(e.0, e.1) == target) .any(|e| normalized_edge(e.0, e.1) == target)
}) { }) {
self.push_undo_snapshot(); self.push_undo_snapshot();
if !self.layout.doors[idx].manual { let layout = &mut self.levels[self.settings.active_level_index];
for edge in door_edges_for( if !layout.doors[idx].manual {
&self.layout.doors[idx], for edge in
self.settings.cols, door_edges_for(&layout.doors[idx], self.settings.cols, self.settings.rows)
self.settings.rows, {
) {
self.suppressed_auto_door_edges.insert(edge); self.suppressed_auto_door_edges.insert(edge);
} }
self.layout.doors.remove(idx); layout.doors.remove(idx);
return; return;
} }
self.layout.doors.remove(idx); layout.doors.remove(idx);
return; return;
} }
} }
@@ -1472,9 +1625,11 @@ impl DungeonApp {
} }
if let Some(corridor_drag) = self.corridor_drag_at_pointer(pointer_pos, geometry) { if let Some(corridor_drag) = self.corridor_drag_at_pointer(pointer_pos, geometry) {
if corridor_drag.corridor_index < self.layout.corridors.len() { let active_index = self.settings.active_level_index;
if corridor_drag.corridor_index < self.levels[active_index].corridors.len() {
self.push_undo_snapshot(); self.push_undo_snapshot();
self.layout.corridors.remove(corridor_drag.corridor_index); let layout = &mut self.levels[self.settings.active_level_index];
layout.corridors.remove(corridor_drag.corridor_index);
self.drag_state = None; self.drag_state = None;
self.refresh_doors(); self.refresh_doors();
} }
@@ -1483,14 +1638,19 @@ impl DungeonApp {
// Remove a room and update corridors/doors to match. // Remove a room and update corridors/doors to match.
fn delete_room(&mut self, room_idx: usize) { fn delete_room(&mut self, room_idx: usize) {
if room_idx >= self.layout.rooms.len() { if self.settings.active_level_index >= self.levels.len() {
return; return;
} }
self.layout.rooms.remove(room_idx); let layout = &mut self.levels[self.settings.active_level_index];
self.layout
if room_idx >= layout.rooms.len() {
return;
}
layout.rooms.remove(room_idx);
layout
.corridors .corridors
.retain(|c| c.start_room_id != room_idx && c.end_room_id != room_idx); .retain(|c| c.start_room_id != room_idx && c.end_room_id != room_idx);
for corridor in &mut self.layout.corridors { for corridor in &mut layout.corridors {
if corridor.start_room_id > room_idx { if corridor.start_room_id > room_idx {
corridor.start_room_id -= 1; corridor.start_room_id -= 1;
} }
@@ -1509,28 +1669,30 @@ impl DungeonApp {
geometry: &GridGeometry, geometry: &GridGeometry,
kind: ManualDoorKind, kind: ManualDoorKind,
) { ) {
self.push_undo_snapshot(); if self.settings.active_level_index >= self.levels.len() {
return;
}
let Some(edge) = self.door_edge_at_pointer(pointer_pos, geometry) else { let Some(edge) = self.door_edge_at_pointer(pointer_pos, geometry) else {
return; return;
}; };
self.push_undo_snapshot();
let layout = &mut self.levels[self.settings.active_level_index];
let target = normalized_edge(edge.0, edge.1); let target = normalized_edge(edge.0, edge.1);
if let Some(idx) = self.layout.doors.iter().position(|d| { if let Some(idx) = layout.doors.iter().position(|d| {
door_edges_for(d, self.settings.cols, self.settings.rows) door_edges_for(d, self.settings.cols, self.settings.rows)
.iter() .iter()
.any(|e| normalized_edge(e.0, e.1) == target) .any(|e| normalized_edge(e.0, e.1) == target)
}) { }) {
let edges = door_edges_for( let edges = door_edges_for(&layout.doors[idx], self.settings.cols, self.settings.rows);
&self.layout.doors[idx],
self.settings.cols,
self.settings.rows,
);
let mut spawned = Vec::new(); let mut spawned = Vec::new();
for edge in edges { for edge in edges {
if normalized_edge(edge.0, edge.1) == target { if normalized_edge(edge.0, edge.1) == target {
continue; continue;
} }
if self if layout
.layout
.doors .doors
.iter() .iter()
.any(|d| normalized_edge(d.from, d.to) == normalized_edge(edge.0, edge.1)) .any(|d| normalized_edge(d.from, d.to) == normalized_edge(edge.0, edge.1))
@@ -1548,7 +1710,7 @@ impl DungeonApp {
manual: true, manual: true,
}); });
} }
let door = &mut self.layout.doors[idx]; let door = &mut layout.doors[idx];
door.locked = kind == ManualDoorKind::Locked; door.locked = kind == ManualDoorKind::Locked;
door.archway = kind == ManualDoorKind::Archway; door.archway = kind == ManualDoorKind::Archway;
door.secret = kind == ManualDoorKind::Secret; door.secret = kind == ManualDoorKind::Secret;
@@ -1558,13 +1720,13 @@ impl DungeonApp {
for edge in door_edges_for(door, self.settings.cols, self.settings.rows) { for edge in door_edges_for(door, self.settings.cols, self.settings.rows) {
self.suppressed_auto_door_edges.remove(&edge); self.suppressed_auto_door_edges.remove(&edge);
} }
self.layout.doors.extend(spawned); layout.doors.extend(spawned);
return; return;
} }
let corridor_cells = corridor_cells(&self.layout, self.settings.cols, self.settings.rows); let corridor_cells = corridor_cells(layout, self.settings.cols, self.settings.rows);
let a_room = room_index_at_cell(&self.layout.rooms, edge.0); let a_room = room_index_at_cell(&layout.rooms, edge.0);
let b_room = room_index_at_cell(&self.layout.rooms, edge.1); let b_room = room_index_at_cell(&layout.rooms, edge.1);
let a_in_corridor = corridor_cells.contains(&edge.0); let a_in_corridor = corridor_cells.contains(&edge.0);
let b_in_corridor = corridor_cells.contains(&edge.1); let b_in_corridor = corridor_cells.contains(&edge.1);
@@ -1572,7 +1734,7 @@ impl DungeonApp {
return; return;
} }
self.layout.doors.push(layout::Door { layout.doors.push(layout::Door {
from: edge.0, from: edge.0,
to: edge.1, to: edge.1,
width: 1, width: 1,
+44
View File
@@ -142,6 +142,9 @@ pub struct UiSettings {
pub max_start_marker_count: usize, pub max_start_marker_count: usize,
pub min_end_marker_count: usize, pub min_end_marker_count: usize,
pub max_end_marker_count: usize, pub max_end_marker_count: usize,
pub min_levels: usize,
pub max_levels: usize,
pub active_level_index: usize,
pub trap_frequency_percent: usize, pub trap_frequency_percent: usize,
pub min_traps_per_area: usize, pub min_traps_per_area: usize,
pub max_traps_per_area: usize, pub max_traps_per_area: usize,
@@ -196,6 +199,9 @@ impl Default for UiSettings {
max_start_marker_count: 1, max_start_marker_count: 1,
min_end_marker_count: 1, min_end_marker_count: 1,
max_end_marker_count: 1, max_end_marker_count: 1,
min_levels: 1,
max_levels: 1,
active_level_index: 0,
trap_frequency_percent: 20, trap_frequency_percent: 20,
min_traps_per_area: 1, min_traps_per_area: 1,
max_traps_per_area: 3, max_traps_per_area: 3,
@@ -484,6 +490,44 @@ fn draw_generate_tab(
// Render the Layout tab controls. // Render the Layout tab controls.
fn draw_layout_tab(ui: &mut egui::Ui, settings: &mut UiSettings, result: &mut SidePanelResult) { fn draw_layout_tab(ui: &mut egui::Ui, settings: &mut UiSettings, result: &mut SidePanelResult) {
ui.add_space(12.0);
ui.separator();
ui.add_space(8.0);
ui.label(RichText::new("Levels").strong());
ui.add_space(8.0);
ui.label("Min Levels");
ui.horizontal(|ui| {
result.settings_changed |= ui
.add(egui::Slider::new(&mut settings.min_levels, 1..=20).show_value(false))
.changed();
result.settings_changed |= ui
.add(
egui::DragValue::new(&mut settings.min_levels)
.speed(1.0)
.range(1..=100),
)
.changed();
});
ui.add_space(8.0);
ui.label("Max Levels");
ui.horizontal(|ui| {
result.settings_changed |= ui
.add(egui::Slider::new(&mut settings.max_levels, 1..=20).show_value(false))
.changed();
result.settings_changed |= ui
.add(
egui::DragValue::new(&mut settings.max_levels)
.speed(1.0)
.range(1..=100),
)
.changed();
});
ui.add_space(12.0);
ui.separator();
ui.add_space(8.0);
ui.label(RichText::new("Room Settings").strong()); ui.label(RichText::new("Room Settings").strong());
ui.add_space(8.0); ui.add_space(8.0);