diff --git a/src/exporter.rs b/src/exporter.rs index 0fb83b4..64191c7 100644 --- a/src/exporter.rs +++ b/src/exporter.rs @@ -826,6 +826,54 @@ pub fn build_svg(layout: &DungeonLayout, settings: &UiSettings) -> String { settings.colorblind_mode, ); + // Draw stairs and elevators. + 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) + ); + } else { + let fill = if settings.colorblind_mode { + "rgb(180,180,180)" + } else { + "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 + ); + } + } + } + s.push_str("\n"); s } diff --git a/src/layout.rs b/src/layout.rs index c2fece0..7e0fe93 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -61,6 +61,13 @@ pub struct AreaMarker { pub size: usize, } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Staircase { + pub cell: (usize, usize), + pub size: usize, + pub is_elevator: bool, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum WindowSide { Left, @@ -100,6 +107,7 @@ pub struct DungeonLayout { pub trap_markers: Vec, pub monster_markers: Vec, pub packed_rooms: bool, + pub stairs: Vec, } impl Default for DungeonLayout { @@ -115,6 +123,7 @@ impl Default for DungeonLayout { trap_markers: Vec::new(), monster_markers: Vec::new(), packed_rooms: false, + stairs: Vec::new(), } } } @@ -186,6 +195,7 @@ pub fn generate_layout( trap_markers: Vec::new(), monster_markers: Vec::new(), packed_rooms: pack_rooms_without_corridors, + stairs: Vec::new(), }; } @@ -213,6 +223,7 @@ pub fn generate_layout( trap_markers: Vec::new(), monster_markers: Vec::new(), packed_rooms: pack_rooms_without_corridors, + stairs: Vec::new(), }; } @@ -235,6 +246,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(), @@ -306,6 +318,7 @@ pub fn generate_layout( trap_markers: Vec::new(), monster_markers: Vec::new(), packed_rooms: false, + stairs: Vec::new(), }; } @@ -354,6 +367,7 @@ pub fn generate_layout( } let mut layout = DungeonLayout { + stairs: Vec::new(), rooms, corridors, doors: Vec::new(), @@ -983,7 +997,7 @@ fn shared_boundary_edges(a: &Room, b: &Room) -> Vec<((usize, usize), (usize, usi edges } -fn room_index_at_cell(rooms: &[Room], cell: (usize, usize)) -> Option { +pub fn room_index_at_cell(rooms: &[Room], cell: (usize, usize)) -> Option { rooms.iter().position(|room| { cell.0 >= room.x && cell.0 < room.x + room.width diff --git a/src/main.rs b/src/main.rs index a5a68c7..9cd5a2f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,7 +18,7 @@ use layout::{ }; use startend::{ manual_marker_size, manual_monster_marker_size, manual_trap_marker_size, - populate_random_markers, + populate_random_markers, populate_stairs, }; use ui::{AddTool, UiSettings, draw_side_panel}; @@ -379,6 +379,7 @@ fn generate_all_levels(settings: &UiSettings) -> Vec { let layout = populate_random_markers(layout, settings); levels.push(layout); } + let levels = populate_stairs(levels, settings); levels } @@ -2302,6 +2303,88 @@ fn draw_layout( hover_marker, MarkerKind::Monster, ); + + // Draw stairs and elevators. + for stair in &layout.stairs { + draw_staircase(painter, geometry, stair, colorblind_mode); + } +} + +// Draw a staircase or elevator within a room. +fn draw_staircase( + painter: &egui::Painter, + geometry: &GridGeometry, + stair: &layout::Staircase, + colorblind_mode: bool, +) { + let left = geometry.rect.left() + stair.cell.0 as f32 * geometry.cell_size; + let top = geometry.rect.top() + stair.cell.1 as f32 * geometry.cell_size; + let right = left + stair.size as f32 * geometry.cell_size; + 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. + 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) + }; + painter.rect_filled(rect, 4.0, fill); + + // 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) + }; + painter.rect_stroke( + rect, + 4.0, + Stroke::new(2.0, stroke_color), + 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 + }, + ); + } 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), + ); + } + } } // Compute the rectangle for a given grid cell. diff --git a/src/startend.rs b/src/startend.rs index 44d3664..a9f9d73 100644 --- a/src/startend.rs +++ b/src/startend.rs @@ -1,6 +1,10 @@ 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; @@ -345,3 +349,200 @@ fn pick_random_corridor_cell( 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 (and optionally elevators) across all levels after markers are placed. +pub fn populate_stairs( + mut layouts: Vec, + settings: &UiSettings, +) -> Vec { + 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 + } 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; + } + + 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); + } + + 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); + } + } + + layouts +} + +// 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, +) -> 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), + ); + + 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)| layout::Staircase { + cell, + size: random_stair_size( + settings, + seed::derive_seed(settings.seed, STAIR_MARKER_STREAM_BASE + i as u64), + ), + is_elevator, + }) + .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 { + let min_size = settings.min_stair_width.min(settings.min_stair_height); + let max_size = settings.max_stair_width.max(settings.max_stair_height); + let span = max_size - min_size + 1; + min_size + (seed_value as usize % span) +} + +// 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 + } +} diff --git a/src/ui.rs b/src/ui.rs index 3f06757..489118f 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -151,6 +151,13 @@ pub struct UiSettings { pub monster_frequency_percent: usize, pub min_monsters_per_area: usize, pub max_monsters_per_area: usize, + pub min_stair_width: usize, + pub max_stair_width: usize, + pub min_stair_height: usize, + pub max_stair_height: usize, + pub min_stairs_per_level: usize, + pub max_stairs_per_level: usize, + pub sync_stairs_across_levels: bool, active_tab: Tab, } @@ -208,6 +215,13 @@ impl Default for UiSettings { monster_frequency_percent: 15, min_monsters_per_area: 1, max_monsters_per_area: 2, + min_stair_width: 1, + max_stair_width: 2, + min_stair_height: 2, + max_stair_height: 3, + min_stairs_per_level: 1, + max_stairs_per_level: 1, + sync_stairs_across_levels: false, active_tab: Tab::Generate, } } @@ -810,6 +824,114 @@ fn draw_layout_tab(ui: &mut egui::Ui, settings: &mut UiSettings, result: &mut Si ) .changed(); } + + ui.add_space(12.0); + ui.separator(); + ui.add_space(8.0); + ui.label(RichText::new("Staircase Settings").strong()); + ui.add_space(8.0); + + result.settings_changed |= ui + .checkbox( + &mut settings.sync_stairs_across_levels, + "Sync Stairs Across All Levels", + ) + .changed(); + + ui.add_space(8.0); + ui.label("Min Stair Width"); + ui.horizontal(|ui| { + result.settings_changed |= ui + .add(egui::Slider::new(&mut settings.min_stair_width, 1..=5).show_value(false)) + .changed(); + result.settings_changed |= ui + .add( + egui::DragValue::new(&mut settings.min_stair_width) + .speed(1.0) + .range(1..=10), + ) + .changed(); + }); + + ui.add_space(8.0); + ui.label("Max Stair Width"); + ui.horizontal(|ui| { + result.settings_changed |= ui + .add(egui::Slider::new(&mut settings.max_stair_width, 1..=5).show_value(false)) + .changed(); + result.settings_changed |= ui + .add( + egui::DragValue::new(&mut settings.max_stair_width) + .speed(1.0) + .range(1..=10), + ) + .changed(); + }); + + ui.add_space(8.0); + ui.label("Min Stair Height"); + ui.horizontal(|ui| { + result.settings_changed |= ui + .add(egui::Slider::new(&mut settings.min_stair_height, 1..=5).show_value(false)) + .changed(); + result.settings_changed |= ui + .add( + egui::DragValue::new(&mut settings.min_stair_height) + .speed(1.0) + .range(1..=10), + ) + .changed(); + }); + + ui.add_space(8.0); + ui.label("Max Stair Height"); + ui.horizontal(|ui| { + result.settings_changed |= ui + .add(egui::Slider::new(&mut settings.max_stair_height, 1..=5).show_value(false)) + .changed(); + result.settings_changed |= ui + .add( + egui::DragValue::new(&mut settings.max_stair_height) + .speed(1.0) + .range(1..=10), + ) + .changed(); + }); + + ui.add_space(8.0); + ui.label("Min Stairs Per Level"); + ui.horizontal(|ui| { + result.settings_changed |= ui + .add(egui::Slider::new(&mut settings.min_stairs_per_level, 0..=10).show_value(false)) + .changed(); + result.settings_changed |= ui + .add( + egui::DragValue::new(&mut settings.min_stairs_per_level) + .speed(1.0) + .range(0..=20), + ) + .changed(); + }); + + ui.add_space(8.0); + ui.label("Max Stairs Per Level"); + ui.horizontal(|ui| { + result.settings_changed |= ui + .add(egui::Slider::new(&mut settings.max_stairs_per_level, 0..=10).show_value(false)) + .changed(); + result.settings_changed |= ui + .add( + egui::DragValue::new(&mut settings.max_stairs_per_level) + .speed(1.0) + .range(0..=20), + ) + .changed(); + }); + + ui.add_space(8.0); + result.settings_changed |= ui + .checkbox(&mut settings.windows_enabled, "Enable Elevators") + .changed(); } // Render the Add tab controls.