diff --git a/src/layout.rs b/src/layout.rs index df99884..ad3ee22 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -128,6 +128,30 @@ impl Default for DungeonLayout { } } +impl DungeonLayout { + // Build an empty layout for the current packing mode. + pub(crate) fn empty(packed_rooms: bool) -> Self { + Self { + packed_rooms, + ..Self::default() + } + } + + // Build a layout from generated rooms and corridors. + fn from_generated_parts( + rooms: Vec, + corridors: Vec, + packed_rooms: bool, + ) -> Self { + Self { + rooms, + corridors, + packed_rooms, + ..Self::default() + } + } +} + // Collect all room cells except those belonging to excluded room ids. pub fn blocked_room_cells(rooms: &[Room], excluded_room_ids: &[usize]) -> HashSet<(usize, usize)> { let excluded: HashSet = excluded_room_ids.iter().copied().collect(); @@ -184,19 +208,7 @@ pub fn generate_layout( let mut corridors = Vec::new(); if cols < 2 || rows < 2 || target_room_count == 0 { - return DungeonLayout { - rooms, - corridors, - doors: Vec::new(), - windows: Vec::new(), - text_labels: Vec::new(), - start_markers: Vec::new(), - end_markers: Vec::new(), - trap_markers: Vec::new(), - monster_markers: Vec::new(), - packed_rooms: pack_rooms_without_corridors, - stairs: Vec::new(), - }; + return DungeonLayout::empty(pack_rooms_without_corridors); } let mut min_size = min_room_size.max(2); @@ -212,19 +224,7 @@ pub fn generate_layout( } if min_size == 0 || max_size < min_size { - return DungeonLayout { - rooms, - corridors, - doors: Vec::new(), - windows: Vec::new(), - text_labels: Vec::new(), - start_markers: Vec::new(), - end_markers: Vec::new(), - trap_markers: Vec::new(), - monster_markers: Vec::new(), - packed_rooms: pack_rooms_without_corridors, - stairs: Vec::new(), - }; + return DungeonLayout::empty(pack_rooms_without_corridors); } let room_sizes = generate_room_sizes( @@ -245,19 +245,7 @@ pub fn generate_layout( if pack_rooms_without_corridors { rooms = place_packed_rooms(&room_sizes, &room_edges, cols, rows, &mut room_rng); - let mut layout = DungeonLayout { - stairs: Vec::new(), - rooms, - corridors, - doors: Vec::new(), - windows: Vec::new(), - text_labels: Vec::new(), - start_markers: Vec::new(), - end_markers: Vec::new(), - trap_markers: Vec::new(), - monster_markers: Vec::new(), - packed_rooms: true, - }; + let mut layout = DungeonLayout::from_generated_parts(rooms, corridors, true); apply_doors(&mut layout, seed, door_settings, cols, rows); apply_windows(&mut layout, seed, window_settings, cols, rows); return layout; @@ -307,19 +295,7 @@ pub fn generate_layout( } if rooms.len() < 2 { - return DungeonLayout { - rooms, - corridors, - doors: Vec::new(), - windows: Vec::new(), - text_labels: Vec::new(), - start_markers: Vec::new(), - end_markers: Vec::new(), - trap_markers: Vec::new(), - monster_markers: Vec::new(), - packed_rooms: false, - stairs: Vec::new(), - }; + return DungeonLayout::from_generated_parts(rooms, corridors, false); } let mut min_width = min_corridor_width.max(1); @@ -366,19 +342,7 @@ pub fn generate_layout( } } - let mut layout = DungeonLayout { - stairs: Vec::new(), - rooms, - corridors, - doors: Vec::new(), - windows: Vec::new(), - text_labels: Vec::new(), - start_markers: Vec::new(), - end_markers: Vec::new(), - trap_markers: Vec::new(), - monster_markers: Vec::new(), - packed_rooms: false, - }; + let mut layout = DungeonLayout::from_generated_parts(rooms, corridors, false); apply_doors(&mut layout, seed, door_settings, cols, rows); apply_windows(&mut layout, seed, window_settings, cols, rows); layout diff --git a/src/main.rs b/src/main.rs index d944f3a..a3919b9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -72,11 +72,9 @@ impl Default for DungeonApp { // Build the app with persisted settings and a generated layout. fn default() -> Self { let settings = settings::load_settings().unwrap_or_default(); - let layouts = generate_all_levels(&settings); - let results = populate_stairs(layouts, &settings); let mut app = Self { settings, - levels: results.iter().map(|(l, _)| l.clone()).collect(), + levels: Vec::new(), suppressed_auto_door_edges: HashSet::new(), undo_stack: Vec::new(), redo_stack: Vec::new(), @@ -93,11 +91,8 @@ impl Default for DungeonApp { hover_level_idx: None, hover_stair_idx: None, }; - for (i, (_, modified)) in results.into_iter().enumerate() { - if modified { - app.refresh_level(i); - } - } + app.levels = generate_all_levels(&app.settings); + app.refresh_stairs(); app } } @@ -170,33 +165,9 @@ impl eframe::App for DungeonApp { } } else if panel_result.new_level_clicked { self.push_undo_snapshot(); - let level_index = self.levels.len(); - let level_seed = seed::derive_seed(self.settings.seed, level_index as u64); - let layout = layout::generate_layout( - self.settings.cols, - self.settings.rows, - self.settings.room_count, - level_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), - ); - let layout = populate_random_markers(layout, &self.settings); - 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.levels + .push(generate_level(&self.settings, self.levels.len())); + self.refresh_stairs(); self.settings.active_level_index = self.levels.len() - 1; } if ctx.input(|i| i.key_pressed(egui::Key::Delete) || i.key_pressed(egui::Key::Backspace)) { @@ -385,29 +356,34 @@ fn generate_all_levels(settings: &UiSettings) -> Vec { 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.push(generate_level(settings, i)); } levels } +// Generate one dungeon level from the current settings. +fn generate_level(settings: &UiSettings, level_index: usize) -> DungeonLayout { + let level_seed = seed::derive_seed(settings.seed, level_index 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), + ); + + populate_random_markers(layout, settings) +} + fn clamp_dependent_settings(settings: &mut UiSettings) { if settings.min_room_size > settings.max_room_size { settings.max_room_size = settings.min_room_size; @@ -514,10 +490,8 @@ impl DungeonApp { 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, - ..DungeonLayout::default() - }; + self.levels[self.settings.active_level_index] = + DungeonLayout::empty(self.settings.pack_rooms_without_corridors); self.suppressed_auto_door_edges.clear(); self.reset_transient_state(); } @@ -526,14 +500,8 @@ impl DungeonApp { fn regenerate_layout(&mut self) { self.drag_state = None; self.suppressed_auto_door_edges.clear(); - 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); - } - } + self.levels = generate_all_levels(&self.settings); + self.refresh_stairs(); if self.settings.active_level_index >= self.levels.len() { self.settings.active_level_index = 0; } @@ -882,6 +850,18 @@ impl DungeonApp { ); } + // Sync stair placement across all generated levels. + fn refresh_stairs(&mut self) { + let results = populate_stairs(self.levels.clone(), &self.settings); + self.levels = results.iter().map(|(layout, _)| layout.clone()).collect(); + + for (level_idx, (_, modified)) in results.into_iter().enumerate() { + if modified { + self.refresh_level(level_idx); + } + } + } + fn refresh_doors(&mut self) { if self.settings.active_level_index >= self.levels.len() { return; diff --git a/src/startend.rs b/src/startend.rs index 763bba6..ce76bb1 100644 --- a/src/startend.rs +++ b/src/startend.rs @@ -242,103 +242,126 @@ fn pick_room_index(available_rooms: &[usize], room_count: usize, seed_value: u64 } pub fn populate_random_traps(mut layout: DungeonLayout, settings: &UiSettings) -> DungeonLayout { - if settings.trap_frequency_percent == 0 { - return layout; - } - - let mut trap_markers = Vec::new(); - - // Rooms - for (idx, room) in layout.rooms.iter().enumerate() { - let seed = seed::derive_seed(settings.seed, TRAP_ROOM_STREAM_BASE + idx as u64); - if (seed % 100) < settings.trap_frequency_percent as u64 { - let count = random_range_inclusive( - settings.min_traps_per_area, - settings.max_traps_per_area, - seed.rotate_left(13), - ); - for i in 0..count { - trap_markers.push(marker_in_room( - room, - 1, - 1, - seed.wrapping_add(i as u64).rotate_right(7), - )); - } - } - } - - // Corridors - for (idx, corridor) in layout.corridors.iter().enumerate() { - let seed = seed::derive_seed(settings.seed, TRAP_CORRIDOR_STREAM_BASE + idx as u64); - if (seed % 100) < settings.trap_frequency_percent as u64 { - let count = random_range_inclusive( - settings.min_traps_per_area, - settings.max_traps_per_area, - seed.rotate_left(13), - ); - for i in 0..count { - if let Some(cell) = pick_random_corridor_cell(corridor, seed.wrapping_add(i as u64)) - { - trap_markers.push(layout::AreaMarker { cell, size: 1 }); - } - } - } - } - - layout.trap_markers = trap_markers; + layout.trap_markers = populate_random_area_markers( + &layout, + settings.seed, + settings.trap_frequency_percent, + settings.min_traps_per_area, + settings.max_traps_per_area, + TRAP_ROOM_STREAM_BASE, + TRAP_CORRIDOR_STREAM_BASE, + ); layout } pub fn populate_random_monsters(mut layout: DungeonLayout, settings: &UiSettings) -> DungeonLayout { - if settings.monster_frequency_percent == 0 { - return layout; - } - - let mut monster_markers = Vec::new(); - - // Rooms - for (idx, room) in layout.rooms.iter().enumerate() { - let seed = seed::derive_seed(settings.seed, MONSTER_ROOM_STREAM_BASE + idx as u64); - if (seed % 100) < settings.monster_frequency_percent as u64 { - let count = random_range_inclusive( - settings.min_monsters_per_area, - settings.max_monsters_per_area, - seed.rotate_left(13), - ); - for i in 0..count { - monster_markers.push(marker_in_room( - room, - 1, - 1, - seed.wrapping_add(i as u64).rotate_right(7), - )); - } - } - } - - // Corridors - for (idx, corridor) in layout.corridors.iter().enumerate() { - let seed = seed::derive_seed(settings.seed, MONSTER_CORRIDOR_STREAM_BASE + idx as u64); - if (seed % 100) < settings.monster_frequency_percent as u64 { - let count = random_range_inclusive( - settings.min_monsters_per_area, - settings.max_monsters_per_area, - seed.rotate_left(13), - ); - for i in 0..count { - if let Some(cell) = pick_random_corridor_cell(corridor, seed.wrapping_add(i as u64)) - { - monster_markers.push(layout::AreaMarker { cell, size: 1 }); - } - } - } - } - - layout.monster_markers = monster_markers; + layout.monster_markers = populate_random_area_markers( + &layout, + settings.seed, + settings.monster_frequency_percent, + settings.min_monsters_per_area, + settings.max_monsters_per_area, + MONSTER_ROOM_STREAM_BASE, + MONSTER_CORRIDOR_STREAM_BASE, + ); layout } +// Populate simple one-cell area markers in rooms and corridors. +fn populate_random_area_markers( + layout: &DungeonLayout, + master_seed: u64, + frequency_percent: usize, + min_count: usize, + max_count: usize, + room_stream_base: u64, + corridor_stream_base: u64, +) -> Vec { + if frequency_percent == 0 { + return Vec::new(); + } + + let mut markers = Vec::new(); + + for (idx, room) in layout.rooms.iter().enumerate() { + let area_seed = seed::derive_seed(master_seed, room_stream_base + idx as u64); + append_room_markers( + &mut markers, + room, + area_seed, + frequency_percent, + min_count, + max_count, + ); + } + + for (idx, corridor) in layout.corridors.iter().enumerate() { + let area_seed = seed::derive_seed(master_seed, corridor_stream_base + idx as u64); + append_corridor_markers( + &mut markers, + corridor, + area_seed, + frequency_percent, + min_count, + max_count, + ); + } + + markers +} + +// Add room markers when the room passes the frequency roll. +fn append_room_markers( + markers: &mut Vec, + room: &layout::Room, + area_seed: u64, + frequency_percent: usize, + min_count: usize, + max_count: usize, +) { + if !passes_frequency_roll(area_seed, frequency_percent) { + return; + } + + let count = random_range_inclusive(min_count, max_count, area_seed.rotate_left(13)); + for offset in 0..count { + markers.push(marker_in_room( + room, + 1, + 1, + area_seed.wrapping_add(offset as u64).rotate_right(7), + )); + } +} + +// Add corridor markers when the corridor passes the frequency roll. +fn append_corridor_markers( + markers: &mut Vec, + corridor: &layout::Corridor, + area_seed: u64, + frequency_percent: usize, + min_count: usize, + max_count: usize, +) { + if !passes_frequency_roll(area_seed, frequency_percent) { + return; + } + + let count = random_range_inclusive(min_count, max_count, area_seed.rotate_left(13)); + for offset in 0..count { + if let Some(cell) = + pick_random_corridor_cell(corridor, area_seed.wrapping_add(offset as u64)) + { + markers.push(layout::AreaMarker { cell, size: 1 }); + } + } +} + +// Check whether an area spawns markers. +fn passes_frequency_roll(area_seed: u64, frequency_percent: usize) -> bool { + (area_seed % 100) < frequency_percent as u64 +} + fn pick_random_corridor_cell( corridor: &layout::Corridor, seed_value: u64,