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.