syncronized staircases on multiple levels and removed elevators as redundant

This commit is contained in:
grimsace
2026-04-27 10:57:31 -05:00
parent 182e2fe0a9
commit 277f8f5391
5 changed files with 225 additions and 163 deletions
+16 -35
View File
@@ -826,51 +826,32 @@ pub fn build_svg(layout: &DungeonLayout, settings: &UiSettings) -> String {
settings.colorblind_mode, settings.colorblind_mode,
); );
// Draw stairs and elevators. // Draw stairs.
for stair in &layout.stairs { for stair in &layout.stairs {
let x = g.left() + stair.cell.0 as f32 * g.cell; let x = g.left() + stair.cell.0 as f32 * g.cell;
let y = g.top() + stair.cell.1 as f32 * g.cell; let y = g.top() + stair.cell.1 as f32 * g.cell;
let w = stair.size as f32 * g.cell; let w = stair.size as f32 * g.cell;
let h = stair.size as f32 * g.cell; let h = stair.size as f32 * g.cell;
if stair.is_elevator { let fill = if settings.colorblind_mode {
let fill = if settings.colorblind_mode { "rgb(180,180,180)"
"rgb(180,180,180)"
} else {
"rgb(100,150,200)"
};
let _ = writeln!(
s,
"<rect x='{x}' y='{y}' width='{w}' height='{h}' fill='{fill}' fill-opacity='0.35' stroke='rgb(100,150,200)' stroke-width='2'/>"
);
let _ = writeln!(
s,
"<text x='{cx}' y='{cy}' text-anchor='middle' dominant-baseline='middle' fill='white' font-size='{fs}'>E</text>",
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 { } else {
let fill = if settings.colorblind_mode { "rgb(200,150,50)"
"rgb(180,180,180)" };
} else { let _ = writeln!(
"rgb(200,150,50)" s,
}; "<rect x='{x}' y='{y}' width='{w}' height='{h}' fill='{fill}' fill-opacity='0.35' stroke='rgb(150,100,30)' stroke-width='2'/>"
);
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!( let _ = writeln!(
s, s,
"<rect x='{x}' y='{y}' width='{w}' height='{h}' fill='{fill}' fill-opacity='0.35' stroke='rgb(150,100,30)' stroke-width='2'/>" "<line x1='{x1}' y1='{sy}' x2='{x2}' y2='{sy}' stroke='rgb(150,100,30)' stroke-width='1.5'/>",
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,
"<line x1='{x1}' y1='{sy}' x2='{x2}' y2='{sy}' stroke='rgb(150,100,30)' stroke-width='1.5'/>",
x1 = x + 2.0,
x2 = x + w - 2.0
);
}
} }
} }
-1
View File
@@ -65,7 +65,6 @@ pub struct AreaMarker {
pub struct Staircase { pub struct Staircase {
pub cell: (usize, usize), pub cell: (usize, usize),
pub size: usize, pub size: usize,
pub is_elevator: bool,
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
+17 -40
View File
@@ -2323,11 +2323,9 @@ fn draw_staircase(
let bottom = top + 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)); 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 { let fill = if colorblind_mode {
Color32::from_rgb(180, 180, 180).gamma_multiply(0.4) 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 { } else {
Color32::from_rgb(200, 150, 50).gamma_multiply(0.35) Color32::from_rgb(200, 150, 50).gamma_multiply(0.35)
}; };
@@ -2336,8 +2334,6 @@ fn draw_staircase(
// Draw border. // Draw border.
let stroke_color = if colorblind_mode { let stroke_color = if colorblind_mode {
Color32::BLACK Color32::BLACK
} else if stair.is_elevator {
Color32::from_rgb(100, 150, 200)
} else { } else {
Color32::from_rgb(200, 150, 50) Color32::from_rgb(200, 150, 50)
}; };
@@ -2348,42 +2344,23 @@ fn draw_staircase(
egui::StrokeKind::Middle, egui::StrokeKind::Middle,
); );
// Draw stair lines or elevator symbol. // Draw stair lines (horizontal steps).
if stair.is_elevator { let steps = (stair.size as f32 * 2.0).round() as usize;
// Draw elevator symbol: a rectangle with arrows. let step_height = (rect.height() / (steps as f32)).max(1.0);
let center = rect.center(); let line_color = if colorblind_mode {
let text = "E"; Color32::BLACK
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 { } else {
// Draw stair lines (horizontal steps). Color32::from_rgb(150, 100, 30)
let steps = (stair.size as f32 * 2.0).round() as usize; };
let step_height = (rect.height() / (steps as f32)).max(1.0); for i in 0..steps {
let line_color = if colorblind_mode { let y = rect.top() + (i as f32 * step_height) + step_height * 0.5;
Color32::BLACK painter.line_segment(
} else { [
Color32::from_rgb(150, 100, 30) egui::pos2(rect.left() + 2.0, y),
}; egui::pos2(rect.right() - 2.0, y),
for i in 0..steps { ],
let y = rect.top() + (i as f32 * step_height) + step_height * 0.5; Stroke::new(1.5, line_color),
painter.line_segment( );
[
egui::pos2(rect.left() + 2.0, y),
egui::pos2(rect.right() - 2.0, y),
],
Stroke::new(1.5, line_color),
);
}
} }
} }
+149 -43
View File
@@ -353,70 +353,171 @@ fn pick_random_corridor_cell(
// Constant for stair marker stream base // Constant for stair marker stream base
const STAIR_MARKER_STREAM_BASE: u64 = 21_000; 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( pub fn populate_stairs(
mut layouts: Vec<DungeonLayout>, mut layouts: Vec<DungeonLayout>,
settings: &UiSettings, settings: &UiSettings,
) -> Vec<DungeonLayout> { ) -> Vec<DungeonLayout> {
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 { if settings.min_stairs_per_level == 0 && settings.max_stairs_per_level == 0 {
return layouts; return layouts;
} }
let stair_count = if settings.min_stairs_per_level == settings.max_stairs_per_level { let num_levels = layouts.len();
settings.min_stairs_per_level 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<layout::Staircase>> = 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 { } else {
let range_seed = seed::derive_seed(settings.seed, 0x2A_3B_4_u64); // Independent positions for each gap.
(range_seed as usize % (settings.max_stairs_per_level - settings.min_stairs_per_level + 1)) for i in 0..sets_to_gen {
+ settings.min_stairs_per_level 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);
if stair_count == 0 { let stairs =
return layouts; pick_stair_positions(&layouts[layout_idx], settings, stair_count, i as u64);
all_stair_sets.push(stairs);
}
} }
let is_elevator = settings.windows_enabled; // Assign stairs to levels.
if num_levels == 1 {
// If syncing stairs, pick the same stair positions for all levels. layouts[0].stairs.extend(all_stair_sets[0].clone());
let mut sync_stair_cells: Vec<layout::Staircase> = Vec::new(); } else {
if settings.sync_stairs_across_levels && !layouts.is_empty() { for i in 0..num_gaps {
let base_settings = settings.clone(); let stairs = &all_stair_sets[i];
let base_layout = layouts[0].clone(); // These stairs connect level i to i+1.
sync_stair_cells = layouts[i].stairs.extend(stairs.clone());
pick_stair_positions(&base_layout, &base_settings, stair_count, is_elevator); layouts[i + 1].stairs.extend(stairs.clone());
}
} }
for (level_idx, layout) in layouts.iter_mut().enumerate() { // De-duplicate stairs at the same location on the same level (can happen if synced).
if settings.sync_stairs_across_levels && !sync_stair_cells.is_empty() { for layout in &mut layouts {
// Use the synced stair positions. let mut seen = HashSet::new();
layout.stairs = sync_stair_cells layout.stairs.retain(|s| seen.insert(s.cell));
.iter() }
.map(|stair| layout::Staircase {
cell: stair.cell, // Ensure every stair is inside a room on its level.
size: random_stair_size( for layout in &mut layouts {
settings, let stairs_clone = layout.stairs.clone();
seed::derive_seed( for stair in &stairs_clone {
settings.seed, ensure_stair_in_room(layout, stair);
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 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. // Pick random cells within rooms that are valid for stair placement and return Staircase objects.
fn pick_stair_positions( fn pick_stair_positions(
layout: &DungeonLayout, layout: &DungeonLayout,
settings: &UiSettings, settings: &UiSettings,
count: usize, count: usize,
is_elevator: bool, seed_offset: u64,
) -> Vec<layout::Staircase> { ) -> Vec<layout::Staircase> {
if layout.rooms.is_empty() || count == 0 { if layout.rooms.is_empty() || count == 0 {
return Vec::new(); return Vec::new();
@@ -444,7 +545,10 @@ fn pick_stair_positions(
let mut shuffled = valid_cells.clone(); let mut shuffled = valid_cells.clone();
shuffle_with_seed( shuffle_with_seed(
&mut shuffled, &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); let max_size = settings.max_stair_width.max(settings.max_stair_height);
@@ -486,9 +590,11 @@ fn pick_stair_positions(
cell, cell,
size: random_stair_size( size: random_stair_size(
settings, 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() .collect()
} }
+43 -44
View File
@@ -221,7 +221,7 @@ impl Default for UiSettings {
max_stair_height: 3, max_stair_height: 3,
min_stairs_per_level: 1, min_stairs_per_level: 1,
max_stairs_per_level: 1, max_stairs_per_level: 1,
sync_stairs_across_levels: false, sync_stairs_across_levels: true,
active_tab: Tab::Generate, active_tab: Tab::Generate,
} }
} }
@@ -267,47 +267,51 @@ pub fn draw_side_panel(
.default_width(280.0) .default_width(280.0)
.show_separator_line(true) .show_separator_line(true)
.show(ctx, |ui| { .show(ctx, |ui| {
ui.horizontal(|ui| { egui::ScrollArea::vertical().show(ui, |ui| {
let is_generate = settings.active_tab == Tab::Generate; ui.horizontal(|ui| {
let is_layout = settings.active_tab == Tab::Layout; let is_generate = settings.active_tab == Tab::Generate;
let is_layout = settings.active_tab == Tab::Layout;
if ui.selectable_label(is_generate, "Generate").clicked() { if ui.selectable_label(is_generate, "Generate").clicked() {
settings.active_tab = Tab::Generate; settings.active_tab = Tab::Generate;
} }
if ui.selectable_label(is_layout, "Layout").clicked() { if ui.selectable_label(is_layout, "Layout").clicked() {
settings.active_tab = Tab::Layout; settings.active_tab = Tab::Layout;
} }
if ui if ui
.selectable_label(settings.active_tab == Tab::StartAndEnd, "Start & End") .selectable_label(settings.active_tab == Tab::StartAndEnd, "Start & End")
.clicked() .clicked()
{ {
settings.active_tab = Tab::StartAndEnd; settings.active_tab = Tab::StartAndEnd;
} }
if ui if ui
.selectable_label( .selectable_label(
settings.active_tab == Tab::MonstersAndTraps, settings.active_tab == Tab::MonstersAndTraps,
"Monsters/Traps", "Monsters/Traps",
) )
.clicked() .clicked()
{ {
settings.active_tab = Tab::MonstersAndTraps; settings.active_tab = Tab::MonstersAndTraps;
} }
if ui if ui
.selectable_label(settings.active_tab == Tab::Add, "Add") .selectable_label(settings.active_tab == Tab::Add, "Add")
.clicked() .clicked()
{ {
settings.active_tab = Tab::Add; 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 result
@@ -927,11 +931,6 @@ fn draw_layout_tab(ui: &mut egui::Ui, settings: &mut UiSettings, result: &mut Si
) )
.changed(); .changed();
}); });
ui.add_space(8.0);
result.settings_changed |= ui
.checkbox(&mut settings.windows_enabled, "Enable Elevators")
.changed();
} }
// Render the Add tab controls. // Render the Add tab controls.