From f53b639d87972d7368597ca2d20e4a721a0cff23 Mon Sep 17 00:00:00 2001 From: grimsace Date: Tue, 28 Apr 2026 14:42:47 -0500 Subject: [PATCH] merged start/end into layout to make things consistant --- src/layout.rs | 657 +++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 6 +- src/startend.rs | 698 ------------------------------------------------ 3 files changed, 658 insertions(+), 703 deletions(-) delete mode 100644 src/startend.rs diff --git a/src/layout.rs b/src/layout.rs index ad3ee22..63693cc 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -2,6 +2,7 @@ use serde::{Deserialize, Serialize}; use std::collections::{HashSet, VecDeque}; use crate::seed; +use crate::ui::UiSettings; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Room { @@ -1086,6 +1087,662 @@ pub fn corridor_cells(layout: &DungeonLayout, cols: usize, rows: usize) -> HashS cells } +const START_COUNT_STREAM: u64 = 10_001; +const END_COUNT_STREAM: u64 = 10_002; +const START_ROOM_STREAM_BASE: u64 = 11_000; +const END_ROOM_STREAM_BASE: u64 = 12_000; +const EXTRA_START_ROOM_STREAM_BASE: u64 = 13_000; +const EXTRA_START_MARKER_STREAM_BASE: u64 = 14_000; +const EXTRA_END_ROOM_STREAM_BASE: u64 = 15_000; +const EXTRA_END_MARKER_STREAM_BASE: u64 = 16_000; +const TRAP_ROOM_STREAM_BASE: u64 = 17_000; +const TRAP_CORRIDOR_STREAM_BASE: u64 = 18_000; +const MONSTER_ROOM_STREAM_BASE: u64 = 19_000; +const MONSTER_CORRIDOR_STREAM_BASE: u64 = 20_000; +const STAIR_MARKER_STREAM_BASE: u64 = 21_000; + +// Populate generated start and end markers. +pub fn populate_random_markers(mut layout: DungeonLayout, settings: &UiSettings) -> DungeonLayout { + layout.start_markers.clear(); + layout.end_markers.clear(); + layout.trap_markers.clear(); + layout.monster_markers.clear(); + + if layout.rooms.is_empty() { + return layout; + } + + let start_count = random_range_inclusive( + settings.min_start_marker_count, + settings.max_start_marker_count, + seed::derive_seed(settings.seed, START_COUNT_STREAM), + ); + let end_count = random_range_inclusive( + settings.min_end_marker_count, + settings.max_end_marker_count, + seed::derive_seed(settings.seed, END_COUNT_STREAM), + ); + let pair_count = start_count.min(end_count); + + let mut available_start_rooms: Vec = (0..layout.rooms.len()).collect(); + let mut available_end_rooms: Vec = (0..layout.rooms.len()).collect(); + + for pair_idx in 0..pair_count { + let (start_room_idx, end_room_idx) = + farthest_room_pair(&layout.rooms, &available_start_rooms, &available_end_rooms); + let start_room = &layout.rooms[start_room_idx]; + let end_room = &layout.rooms[end_room_idx]; + layout.start_markers.push(marker_in_room( + start_room, + settings.min_start_marker_size, + settings.max_start_marker_size, + seed::derive_seed(settings.seed, START_ROOM_STREAM_BASE + pair_idx as u64), + )); + layout.end_markers.push(marker_in_room( + end_room, + settings.min_end_marker_size, + settings.max_end_marker_size, + seed::derive_seed(settings.seed, END_ROOM_STREAM_BASE + pair_idx as u64), + )); + consume_room(&mut available_start_rooms, start_room_idx); + consume_room(&mut available_end_rooms, end_room_idx); + } + + assign_extra_markers( + &mut layout.start_markers, + &layout.rooms, + &mut available_start_rooms, + pair_count, + start_count, + settings.min_start_marker_size, + settings.max_start_marker_size, + settings.seed, + EXTRA_START_ROOM_STREAM_BASE, + EXTRA_START_MARKER_STREAM_BASE, + ); + assign_extra_markers( + &mut layout.end_markers, + &layout.rooms, + &mut available_end_rooms, + pair_count, + end_count, + settings.min_end_marker_size, + settings.max_end_marker_size, + settings.seed, + EXTRA_END_ROOM_STREAM_BASE, + EXTRA_END_MARKER_STREAM_BASE, + ); + + layout = populate_random_traps(layout, settings); + layout = populate_random_monsters(layout, settings); + layout +} + +// Return the manual marker size. +pub fn manual_marker_size(settings: &UiSettings, is_start: bool) -> usize { + if is_start { + settings + .min_start_marker_size + .min(settings.max_start_marker_size) + } else { + settings + .min_end_marker_size + .min(settings.max_end_marker_size) + } + .clamp(1, 10) +} + +// Return the manual trap size. +pub fn manual_trap_marker_size(_settings: &UiSettings) -> usize { + 1 +} + +// Return the manual monster size. +pub fn manual_monster_marker_size(_settings: &UiSettings) -> usize { + 1 +} + +// Pick a deterministic inclusive random value. +fn random_range_inclusive(min: usize, max: usize, seed_value: u64) -> usize { + let min = min.max(1); + let max = max.max(min); + let span = max - min + 1; + min + (seed_value as usize % span) +} + +// Fit a marker inside a room. +fn marker_in_room(room: &Room, min_size: usize, max_size: usize, seed_value: u64) -> AreaMarker { + let room_limit = room.width.min(room.height).max(1); + let min_size = min_size.clamp(1, 10).min(room_limit); + let max_size = max_size.clamp(min_size, 10).min(room_limit); + let size = random_range_inclusive(min_size, max_size, seed_value.rotate_left(7)); + let x_span = room.width.saturating_sub(size); + let y_span = room.height.saturating_sub(size); + let x = room.x + + if x_span == 0 { + 0 + } else { + seed_value as usize % (x_span + 1) + }; + let y = room.y + + if y_span == 0 { + 0 + } else { + seed_value.rotate_left(19) as usize % (y_span + 1) + }; + AreaMarker { cell: (x, y), size } +} + +fn assign_extra_markers( + markers: &mut Vec, + rooms: &[Room], + available_rooms: &mut Vec, + start_idx: usize, + end_idx: usize, + min_size: usize, + max_size: usize, + seed_base: u64, + room_stream_base: u64, + marker_stream_base: u64, +) { + for idx in start_idx..end_idx { + let room_idx = pick_room_index( + available_rooms, + rooms.len(), + seed::derive_seed(seed_base, room_stream_base + idx as u64), + ); + markers.push(marker_in_room( + &rooms[room_idx], + min_size, + max_size, + seed::derive_seed(seed_base, marker_stream_base + idx as u64), + )); + consume_room(available_rooms, room_idx); + } +} + +// Match each start and end pair. +fn farthest_room_pair( + rooms: &[Room], + available_start_rooms: &[usize], + available_end_rooms: &[usize], +) -> (usize, usize) { + let start_rooms = if available_start_rooms.is_empty() { + (0..rooms.len()).collect::>() + } else { + available_start_rooms.to_vec() + }; + let end_rooms = if available_end_rooms.is_empty() { + (0..rooms.len()).collect::>() + } else { + available_end_rooms.to_vec() + }; + + let mut best = (start_rooms[0], end_rooms[0]); + let mut best_dist = 0usize; + for &start_idx in &start_rooms { + for &end_idx in &end_rooms { + if rooms.len() > 1 && start_idx == end_idx { + continue; + } + let dist = room_distance_sq(&rooms[start_idx], &rooms[end_idx]); + if dist > best_dist { + best = (start_idx, end_idx); + best_dist = dist; + } + } + } + best +} + +fn room_distance_sq(a: &Room, b: &Room) -> usize { + let ac = a.center_cell(); + let bc = b.center_cell(); + let dx = ac.0.abs_diff(bc.0); + let dy = ac.1.abs_diff(bc.1); + dx * dx + dy * dy +} + +// Remove a consumed room. +fn consume_room(available_rooms: &mut Vec, room_idx: usize) { + if let Some(pos) = available_rooms.iter().position(|&idx| idx == room_idx) { + available_rooms.remove(pos); + } +} + +// Pick a room index from the pool. +fn pick_room_index(available_rooms: &[usize], room_count: usize, seed_value: u64) -> usize { + if !available_rooms.is_empty() { + available_rooms[seed_value as usize % available_rooms.len()] + } else { + seed_value as usize % room_count.max(1) + } +} + +// Populate random trap markers. +pub fn populate_random_traps(mut layout: DungeonLayout, settings: &UiSettings) -> DungeonLayout { + 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 +} + +// Populate random monster markers. +pub fn populate_random_monsters(mut layout: DungeonLayout, settings: &UiSettings) -> DungeonLayout { + 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 one-cell area markers. +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 after a frequency roll. +fn append_room_markers( + markers: &mut Vec, + room: &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 after a frequency roll. +fn append_corridor_markers( + markers: &mut Vec, + corridor: &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(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: &Corridor, seed_value: u64) -> Option<(usize, usize)> { + if corridor.path.is_empty() { + return None; + } + let idx = seed_value as usize % corridor.path.len(); + Some(corridor.path[idx]) +} + +// Populate stairs across all levels. +pub fn populate_stairs( + mut layouts: Vec, + settings: &UiSettings, +) -> Vec<(DungeonLayout, bool)> { + if layouts.is_empty() { + return Vec::new(); + } + + for layout in &mut layouts { + layout.stairs.clear(); + } + + if settings.min_stairs_per_level == 0 && settings.max_stairs_per_level == 0 { + return layouts.into_iter().map(|layout| (layout, false)).collect(); + } + + let num_levels = layouts.len(); + let num_gaps = num_levels.saturating_sub(1); + 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 { + let stair_count = get_stair_count(settings, 0); + let synced_stairs = pick_stair_positions(&layouts[0], settings, stair_count, 0); + for _ in 0..sets_to_gen { + all_stair_sets.push(synced_stairs.clone()); + } + } else { + for gap_idx in 0..sets_to_gen { + let stair_count = get_stair_count(settings, gap_idx); + let layout_idx = gap_idx.min(num_levels - 1); + let stairs = + pick_stair_positions(&layouts[layout_idx], settings, stair_count, gap_idx as u64); + all_stair_sets.push(stairs); + } + } + + if num_levels == 1 { + layouts[0].stairs.extend(all_stair_sets[0].clone()); + } else { + for gap_idx in 0..num_gaps { + let stairs = &all_stair_sets[gap_idx]; + layouts[gap_idx].stairs.extend(stairs.clone()); + layouts[gap_idx + 1].stairs.extend(stairs.clone()); + } + } + + for layout in &mut layouts { + let mut seen = HashSet::new(); + layout.stairs.retain(|stair| seen.insert(stair.cell)); + } + + let mut result = Vec::new(); + for mut layout in layouts { + let mut modified = false; + let stairs = layout.stairs.clone(); + for stair in &stairs { + if ensure_stair_in_room(&mut layout, stair) { + modified = true; + } + } + result.push((layout, modified)); + } + + result +} + +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 stair is inside a room. +pub fn ensure_stair_in_room(layout: &mut DungeonLayout, stair: &Staircase) -> bool { + let stair_x = stair.cell.0; + let stair_y = stair.cell.1; + let stair_width = stair.width; + let stair_height = stair.height; + + for room in &layout.rooms { + if stair_x >= room.x + && stair_y >= room.y + && (stair_x + stair_width) <= room.x + room.width + && (stair_y + stair_height) <= room.y + room.height + { + return false; + } + } + + 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 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_width); + let new_bottom = (room.y + room.height).max(stair_y + stair_height); + + room.x = new_x; + room.y = new_y; + room.width = new_right - new_x; + room.height = new_bottom - new_y; + true + } else { + layout.rooms.push(Room { + x: stair_x, + y: stair_y, + width: stair_width, + height: stair_height, + }); + true + } +} + +// Compute room distance from a stair. +fn room_to_stair_min_dist(room: &Room, stair: &Staircase) -> usize { + let dx = if stair.cell.0 + stair.width <= room.x { + room.x - (stair.cell.0 + stair.width) + } 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.height <= room.y { + room.y - (stair.cell.1 + stair.height) + } else if stair.cell.1 >= room.y + room.height { + stair.cell.1 - (room.y + room.height) + } else { + 0 + }; + + dx + dy +} + +// Pick valid stair positions. +fn pick_stair_positions( + layout: &DungeonLayout, + settings: &UiSettings, + count: usize, + seed_offset: u64, +) -> Vec { + if layout.rooms.is_empty() || count == 0 { + return Vec::new(); + } + + let mut valid_cells = Vec::new(); + for room in &layout.rooms { + let blocked_bottom_row = + has_start_or_end_on_bottom_row(room, &layout.start_markers, &layout.end_markers); + if !blocked_bottom_row { + for x in room.x..(room.x + room.width) { + for y in room.y..(room.y + room.height) { + valid_cells.push((x, y)); + } + } + } + } + + if valid_cells.is_empty() { + return Vec::new(); + } + + let mut shuffled = valid_cells.clone(); + shuffle_with_seed( + &mut shuffled, + settings + .seed + .wrapping_add(STAIR_MARKER_STREAM_BASE) + .wrapping_add(seed_offset), + ); + + let max_size = settings.max_stair_width.max(settings.max_stair_height); + let mut stair_cells = Vec::new(); + let mut used_cells = HashSet::new(); + + for cell in shuffled { + if stair_cells.len() >= count || used_cells.contains(&cell) { + continue; + } + + if let Some(room_idx) = room_index_at_cell(&layout.rooms, cell) { + let room = &layout.rooms[room_idx]; + let available_w = room.x + room.width - cell.0; + let available_h = room.y + room.height - cell.1; + if available_w >= settings.min_stair_width && available_h >= settings.min_stair_height { + stair_cells.push(cell); + for dx in 0..max_size { + for dy in 0..max_size { + used_cells.insert((cell.0 + dx, cell.1 + dy)); + } + } + } + } + } + + stair_cells + .into_iter() + .enumerate() + .map(|(idx, cell)| { + let (width, height) = random_stair_size( + settings, + seed::derive_seed( + settings.seed, + STAIR_MARKER_STREAM_BASE + idx as u64 + seed_offset * 1000, + ), + ); + Staircase { + cell, + width, + height, + } + }) + .collect() +} + +// Check whether markers overlap a room bottom row. +fn has_start_or_end_on_bottom_row( + room: &Room, + start_markers: &[AreaMarker], + end_markers: &[AreaMarker], +) -> bool { + let bottom_row = room.y + room.height - 1; + for marker in start_markers.iter().chain(end_markers.iter()) { + let marker_bottom = marker.cell.1 + marker.size; + if marker.cell.1 <= bottom_row && marker_bottom > bottom_row { + return true; + } + } + false +} + +// Pick a random stair size. +fn random_stair_size(settings: &UiSettings, seed_value: u64) -> (usize, usize) { + let w_span = settings + .max_stair_width + .saturating_sub(settings.min_stair_width) + + 1; + let h_span = settings + .max_stair_height + .saturating_sub(settings.min_stair_height) + + 1; + let width = settings.min_stair_width + (seed_value as usize % w_span); + let height = settings.min_stair_height + (seed_value.rotate_left(17) as usize % h_span); + (width, height) +} + +// Shuffle cells deterministically. +fn shuffle_with_seed(items: &mut [T], seed: u64) { + let mut rng = StairShuffleRng::new(seed); + for idx in (1..items.len()).rev() { + let swap_idx = rng.next_u32() as usize % (idx + 1); + items.swap(idx, swap_idx); + } +} + +struct StairShuffleRng { + state: u64, +} + +impl StairShuffleRng { + // Create the original stair shuffle RNG. + fn new(seed: u64) -> Self { + Self { state: seed } + } + + // Return the next shuffled value. + fn next_u32(&mut self) -> u32 { + self.state = self + .state + .wrapping_mul(6364136223846793005) + .wrapping_add(1442695040888963407); + (self.state >> 32) as u32 + } +} + fn generate_room_sizes( target_room_count: usize, cols: usize, diff --git a/src/main.rs b/src/main.rs index a3919b9..7efc722 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,6 @@ mod layout; mod saveandload; mod seed; mod settings; -mod startend; mod ui; use std::collections::HashSet; @@ -14,11 +13,8 @@ use eframe::egui; use egui::{Color32, Stroke}; use layout::{ DoorSettings, DungeonLayout, WindowSettings, blocked_room_cells, corridor_cells, - shortest_path_cells, -}; -use startend::{ manual_marker_size, manual_monster_marker_size, manual_trap_marker_size, - populate_random_markers, populate_stairs, + populate_random_markers, populate_stairs, shortest_path_cells, }; use ui::{AddTool, UiSettings, draw_side_panel}; diff --git a/src/startend.rs b/src/startend.rs deleted file mode 100644 index ce76bb1..0000000 --- a/src/startend.rs +++ /dev/null @@ -1,698 +0,0 @@ -use crate::layout::{self, DungeonLayout}; -use crate::seed; -use crate::ui::UiSettings; -use std::collections::HashSet; - -// Import room_index_at_cell from layout module. -use crate::layout::room_index_at_cell; - -const START_COUNT_STREAM: u64 = 10_001; -const END_COUNT_STREAM: u64 = 10_002; -const START_ROOM_STREAM_BASE: u64 = 11_000; -const END_ROOM_STREAM_BASE: u64 = 12_000; -const EXTRA_START_ROOM_STREAM_BASE: u64 = 13_000; -const EXTRA_START_MARKER_STREAM_BASE: u64 = 14_000; -const EXTRA_END_ROOM_STREAM_BASE: u64 = 15_000; -const EXTRA_END_MARKER_STREAM_BASE: u64 = 16_000; -const TRAP_ROOM_STREAM_BASE: u64 = 17_000; -const TRAP_CORRIDOR_STREAM_BASE: u64 = 18_000; -const MONSTER_ROOM_STREAM_BASE: u64 = 19_000; -const MONSTER_CORRIDOR_STREAM_BASE: u64 = 20_000; - -// Populate generated start/end markers after the core dungeon layout exists. -pub fn populate_random_markers(mut layout: DungeonLayout, settings: &UiSettings) -> DungeonLayout { - layout.start_markers.clear(); - layout.end_markers.clear(); - layout.trap_markers.clear(); - layout.monster_markers.clear(); - - if layout.rooms.is_empty() { - return layout; - } - - let start_count = random_range_inclusive( - settings.min_start_marker_count, - settings.max_start_marker_count, - seed::derive_seed(settings.seed, START_COUNT_STREAM), - ); - let end_count = random_range_inclusive( - settings.min_end_marker_count, - settings.max_end_marker_count, - seed::derive_seed(settings.seed, END_COUNT_STREAM), - ); - let pair_count = start_count.min(end_count); - - let mut available_start_rooms: Vec = (0..layout.rooms.len()).collect(); - let mut available_end_rooms: Vec = (0..layout.rooms.len()).collect(); - - for pair_idx in 0..pair_count { - let (start_room_idx, end_room_idx) = - farthest_room_pair(&layout.rooms, &available_start_rooms, &available_end_rooms); - let start_room = &layout.rooms[start_room_idx]; - let end_room = &layout.rooms[end_room_idx]; - layout.start_markers.push(marker_in_room( - start_room, - settings.min_start_marker_size, - settings.max_start_marker_size, - seed::derive_seed(settings.seed, START_ROOM_STREAM_BASE + pair_idx as u64), - )); - layout.end_markers.push(marker_in_room( - end_room, - settings.min_end_marker_size, - settings.max_end_marker_size, - seed::derive_seed(settings.seed, END_ROOM_STREAM_BASE + pair_idx as u64), - )); - consume_room(&mut available_start_rooms, start_room_idx); - consume_room(&mut available_end_rooms, end_room_idx); - } - - assign_extra_markers( - &mut layout.start_markers, - &layout.rooms, - &mut available_start_rooms, - pair_count, - start_count, - settings.min_start_marker_size, - settings.max_start_marker_size, - settings.seed, - EXTRA_START_ROOM_STREAM_BASE, - EXTRA_START_MARKER_STREAM_BASE, - ); - assign_extra_markers( - &mut layout.end_markers, - &layout.rooms, - &mut available_end_rooms, - pair_count, - end_count, - settings.min_end_marker_size, - settings.max_end_marker_size, - settings.seed, - EXTRA_END_ROOM_STREAM_BASE, - EXTRA_END_MARKER_STREAM_BASE, - ); - - layout = populate_random_traps(layout, settings); - layout = populate_random_monsters(layout, settings); - - layout -} - -// Manual marker placement uses the configured minimum size for the chosen marker type. -pub fn manual_marker_size(settings: &UiSettings, is_start: bool) -> usize { - if is_start { - settings - .min_start_marker_size - .min(settings.max_start_marker_size) - } else { - settings - .min_end_marker_size - .min(settings.max_end_marker_size) - } - .clamp(1, 10) -} - -pub fn manual_trap_marker_size(_settings: &UiSettings) -> usize { - 1 -} - -pub fn manual_monster_marker_size(_settings: &UiSettings) -> usize { - 1 -} - -// Pick a deterministic inclusive random value from a seed-derived stream. -fn random_range_inclusive(min: usize, max: usize, seed_value: u64) -> usize { - let min = min.max(1); - let max = max.max(min); - let span = max - min + 1; - min + (seed_value as usize % span) -} - -// Fit a marker inside a room and choose a deterministic offset within that room. -fn marker_in_room( - room: &layout::Room, - min_size: usize, - max_size: usize, - seed_value: u64, -) -> layout::AreaMarker { - let room_limit = room.width.min(room.height).max(1); - let min_size = min_size.clamp(1, 10).min(room_limit); - let max_size = max_size.clamp(min_size, 10).min(room_limit); - let size = random_range_inclusive(min_size, max_size, seed_value.rotate_left(7)); - let x_span = room.width.saturating_sub(size); - let y_span = room.height.saturating_sub(size); - let x = room.x - + if x_span == 0 { - 0 - } else { - (seed_value as usize) % (x_span + 1) - }; - let y = room.y - + if y_span == 0 { - 0 - } else { - (seed_value.rotate_left(19) as usize) % (y_span + 1) - }; - layout::AreaMarker { cell: (x, y), size } -} - -fn assign_extra_markers( - markers: &mut Vec, - rooms: &[layout::Room], - available_rooms: &mut Vec, - start_idx: usize, - end_idx: usize, - min_size: usize, - max_size: usize, - seed_base: u64, - room_stream_base: u64, - marker_stream_base: u64, -) { - for idx in start_idx..end_idx { - let room_idx = pick_room_index( - available_rooms, - rooms.len(), - seed::derive_seed(seed_base, room_stream_base + idx as u64), - ); - markers.push(marker_in_room( - &rooms[room_idx], - min_size, - max_size, - seed::derive_seed(seed_base, marker_stream_base + idx as u64), - )); - consume_room(available_rooms, room_idx); - } -} - -// Match each start/end pair to the farthest available room combination. -fn farthest_room_pair( - rooms: &[layout::Room], - available_start_rooms: &[usize], - available_end_rooms: &[usize], -) -> (usize, usize) { - let start_rooms = if available_start_rooms.is_empty() { - (0..rooms.len()).collect::>() - } else { - available_start_rooms.to_vec() - }; - let end_rooms = if available_end_rooms.is_empty() { - (0..rooms.len()).collect::>() - } else { - available_end_rooms.to_vec() - }; - - let mut best = (start_rooms[0], end_rooms[0]); - let mut best_dist = 0usize; - for &start_idx in &start_rooms { - for &end_idx in &end_rooms { - if rooms.len() > 1 && start_idx == end_idx { - continue; - } - let dist = room_distance_sq(&rooms[start_idx], &rooms[end_idx]); - if dist > best_dist { - best = (start_idx, end_idx); - best_dist = dist; - } - } - } - best -} - -fn room_distance_sq(a: &layout::Room, b: &layout::Room) -> usize { - let ac = a.center_cell(); - let bc = b.center_cell(); - let dx = ac.0.abs_diff(bc.0); - let dy = ac.1.abs_diff(bc.1); - dx * dx + dy * dy -} - -// Remove a room from the available set once it has been consumed by a marker assignment. -fn consume_room(available_rooms: &mut Vec, room_idx: usize) { - if let Some(pos) = available_rooms.iter().position(|&idx| idx == room_idx) { - available_rooms.remove(pos); - } -} - -// Fall back to any room when all candidates for a side have been exhausted. -fn pick_room_index(available_rooms: &[usize], room_count: usize, seed_value: u64) -> usize { - if !available_rooms.is_empty() { - available_rooms[seed_value as usize % available_rooms.len()] - } else { - seed_value as usize % room_count.max(1) - } -} - -pub fn populate_random_traps(mut layout: DungeonLayout, settings: &UiSettings) -> DungeonLayout { - 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 { - 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, -) -> Option<(usize, usize)> { - if corridor.path.is_empty() { - return None; - } - let idx = (seed_value as usize) % corridor.path.len(); - Some(corridor.path[idx]) -} - -// Constant for stair marker stream base -const STAIR_MARKER_STREAM_BASE: u64 = 21_000; - -// Populate stairs across all levels after markers are placed. -// Returns a vector of tuples: (DungeonLayout, room_modified) -pub fn populate_stairs( - mut layouts: Vec, - settings: &UiSettings, -) -> Vec<(DungeonLayout, bool)> { - if layouts.is_empty() { - return Vec::new(); - } - - // 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.into_iter().map(|l| (l, false)).collect(); - } - - 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 { - // 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); - } - } - - // 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()); - } - } - - // 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)); - } - - let mut result = Vec::new(); - // Ensure every stair is inside a room on its level. - for mut layout in layouts { - let mut modified = false; - let stairs_clone = layout.stairs.clone(); - for stair in &stairs_clone { - if ensure_stair_in_room(&mut layout, stair) { - modified = true; - } - } - result.push((layout, modified)); - } - - result -} - -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. -// 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_y = stair.cell.1; - let stair_width = stair.width; - let stair_height = stair.height; - - // 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_width) <= (room.x + room.width) - && (stair_y + stair_height) <= (room.y + room.height) - { - return false; - } - } - - // 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_width); - let new_bottom = (room.y + room.height).max(stair_y + stair_height); - - room.x = new_x; - room.y = new_y; - room.width = new_right - new_x; - room.height = new_bottom - new_y; - true - } else { - // Otherwise, create a new room for the staircase. - layout.rooms.push(layout::Room { - x: stair_x, - y: stair_y, - width: stair_width, - height: stair_height, - }); - true - } -} - -// 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.width <= room.x { - room.x - (stair.cell.0 + stair.width) - } 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.height <= room.y { - room.y - (stair.cell.1 + stair.height) - } 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, - seed_offset: u64, -) -> Vec { - if layout.rooms.is_empty() || count == 0 { - return Vec::new(); - } - - // Collect valid room cells (rooms without start/end markers on bottom row). - let mut valid_cells: Vec<(usize, usize)> = Vec::new(); - for room in &layout.rooms { - let has_start_end_bottom = - has_start_or_end_on_bottom_row(room, &layout.start_markers, &layout.end_markers); - if !has_start_end_bottom { - for x in room.x..(room.x + room.width) { - for y in room.y..(room.y + room.height) { - valid_cells.push((x, y)); - } - } - } - } - - if valid_cells.is_empty() { - return Vec::new(); - } - - // Shuffle valid cells deterministically based on seed. - let mut shuffled = valid_cells.clone(); - shuffle_with_seed( - &mut shuffled, - settings - .seed - .wrapping_add(STAIR_MARKER_STREAM_BASE) - .wrapping_add(seed_offset), - ); - - let max_size = settings.max_stair_width.max(settings.max_stair_height); - let mut stair_cells: Vec<(usize, usize)> = Vec::new(); - let mut used_cells: HashSet<(usize, usize)> = HashSet::new(); - - for cell in shuffled { - if stair_cells.len() >= count { - break; - } - if used_cells.contains(&cell) { - continue; - } - // Check if this cell can accommodate a stair of at least min size. - let min_w = settings.min_stair_width; - let min_h = settings.min_stair_height; - let room_idx = room_index_at_cell(&layout.rooms, cell); - if let Some(ri) = room_idx { - let room = &layout.rooms[ri]; - let available_w = room.x + room.width - cell.0; - let available_h = room.y + room.height - cell.1; - if available_w >= min_w && available_h >= min_h { - stair_cells.push(cell); - // Mark occupied cells to avoid overlap. - for dx in 0..max_size { - for dy in 0..max_size { - used_cells.insert((cell.0 + dx, cell.1 + dy)); - } - } - } - } - } - - // Convert cells to Staircase objects with random sizes. - stair_cells - .into_iter() - .enumerate() - .map(|(i, cell)| { - let (width, height) = random_stair_size( - settings, - seed::derive_seed( - settings.seed, - STAIR_MARKER_STREAM_BASE + i as u64 + seed_offset * 1000, - ), - ); - layout::Staircase { - cell, - width, - height, - } - }) - .collect() -} - -// Check if a room has a start or end marker on its bottom row. -fn has_start_or_end_on_bottom_row( - room: &layout::Room, - start_markers: &[layout::AreaMarker], - end_markers: &[layout::AreaMarker], -) -> bool { - let bottom_row = room.y + room.height - 1; - for marker in start_markers.iter().chain(end_markers.iter()) { - let marker_bottom = marker.cell.1 + marker.size; - if marker.cell.1 <= bottom_row && marker_bottom > bottom_row { - // Marker overlaps with bottom row. - return true; - } - } - false -} - -// Pick a random stair size within settings range. -fn random_stair_size(settings: &UiSettings, seed_value: u64) -> (usize, usize) { - let w_span = settings - .max_stair_width - .saturating_sub(settings.min_stair_width) - + 1; - let h_span = settings - .max_stair_height - .saturating_sub(settings.min_stair_height) - + 1; - let width = settings.min_stair_width + (seed_value as usize % w_span); - let height = settings.min_stair_height + (seed_value.rotate_left(17) as usize % h_span); - (width, height) -} - -// Shuffle a vector deterministically using a seed. -fn shuffle_with_seed(vec: &mut [T], seed: u64) { - let mut rng = SimpleRng::new(seed); - for i in (1..vec.len()).rev() { - let j = (rng.next_u32() as usize) % (i + 1); - vec.swap(i, j); - } -} - -// Simple RNG for deterministic shuffling. -struct SimpleRng { - state: u64, -} - -impl SimpleRng { - fn new(seed: u64) -> Self { - Self { state: seed } - } - - fn next_u32(&mut self) -> u32 { - // Simple LCG for deterministic shuffling. - self.state = self - .state - .wrapping_mul(6364136223846793005) - .wrapping_add(1442695040888963407); - (self.state >> 32) as u32 - } -}