made stairs movable

This commit is contained in:
grimsace
2026-04-27 11:16:01 -05:00
parent 277f8f5391
commit c4a8d606e1
3 changed files with 200 additions and 7 deletions
+198 -5
View File
@@ -65,6 +65,7 @@ struct DungeonApp {
hover_text_idx: Option<usize>,
hover_marker: Option<HoverMarker>,
hover_level_idx: Option<usize>,
hover_stair_idx: Option<usize>,
}
impl Default for DungeonApp {
@@ -89,6 +90,7 @@ impl Default for DungeonApp {
hover_text_idx: None,
hover_marker: None,
hover_level_idx: None,
hover_stair_idx: None,
}
}
}
@@ -338,6 +340,7 @@ impl eframe::App for DungeonApp {
self.settings.colorblind_mode,
self.hover_text_idx,
self.hover_marker,
self.hover_stair_idx,
);
}
self.draw_add_overlay(&painter, &geometry);
@@ -430,6 +433,7 @@ impl DungeonApp {
self.hover_text_idx = None;
self.hover_marker = None;
self.hover_level_idx = None;
self.hover_stair_idx = None;
}
fn reset_transient_state(&mut self) {
@@ -549,6 +553,12 @@ impl DungeonApp {
{
self.push_undo_snapshot();
self.drag_state = Some(DragState::Marker(marker_drag));
} else if response.drag_started()
&& let Some(pointer_pos) = response.interact_pointer_pos()
&& let Some(stair_drag) = self.stair_drag_at_pointer(pointer_pos, geometry)
{
self.push_undo_snapshot();
self.drag_state = Some(DragState::Staircase(stair_drag));
} else if response.drag_started()
&& let Some(pointer_pos) = response.interact_pointer_pos()
&& let Some((room_idx, offset_x, offset_y)) =
@@ -579,6 +589,9 @@ impl DungeonApp {
DragState::Corridor(drag) => {
self.drag_corridor_to_pointer(drag, pointer_pos, geometry)
}
DragState::Staircase(drag) => {
self.drag_stair_to_pointer(drag, pointer_pos, geometry)
}
}
}
@@ -1109,6 +1122,16 @@ impl DungeonApp {
self.hover_room_idx = None;
self.hover_corridor_idx = None;
self.hover_door_idx = None;
self.hover_stair_idx = None;
return;
}
self.hover_stair_idx = self.stair_at_pointer(pointer_pos, geometry);
if self.hover_stair_idx.is_some() {
self.hover_room_idx = None;
self.hover_corridor_idx = None;
self.hover_door_idx = None;
self.hover_marker = None;
return;
}
@@ -1240,6 +1263,43 @@ impl DungeonApp {
}
}
fn stair_at_pointer(&self, pointer_pos: egui::Pos2, geometry: &GridGeometry) -> Option<usize> {
let (grid_x, grid_y) = pointer_to_grid(pointer_pos, geometry)?;
let clicked = (grid_x.floor() as usize, grid_y.floor() as usize);
if self.settings.active_level_index >= self.levels.len() {
return None;
}
let layout = &self.levels[self.settings.active_level_index];
layout.stairs.iter().position(|stair| {
clicked.0 >= stair.cell.0
&& clicked.0 < stair.cell.0 + stair.size
&& clicked.1 >= stair.cell.1
&& clicked.1 < stair.cell.1 + stair.size
})
}
fn stair_drag_at_pointer(
&self,
pointer_pos: egui::Pos2,
geometry: &GridGeometry,
) -> Option<StaircaseDragState> {
let (grid_x, grid_y) = pointer_to_grid(pointer_pos, geometry)?;
if self.settings.active_level_index >= self.levels.len() {
return None;
}
let stair_idx = self.stair_at_pointer(pointer_pos, geometry)?;
let layout = &self.levels[self.settings.active_level_index];
let stair = layout.stairs.get(stair_idx)?;
Some(StaircaseDragState {
stair_idx,
offset_x: grid_x - stair.cell.0 as f32,
offset_y: grid_y - stair.cell.1 as f32,
original_cell: stair.cell,
})
}
fn drag_marker_to_pointer(
&mut self,
drag: MarkerDragState,
@@ -1273,6 +1333,86 @@ impl DungeonApp {
marker.cell = (target_x.min(max_x), target_y.min(max_y));
}
fn drag_stair_to_pointer(
&mut self,
drag: StaircaseDragState,
pointer_pos: egui::Pos2,
geometry: &GridGeometry,
) {
let Some((grid_x, grid_y)) = pointer_to_grid(pointer_pos, geometry) else {
return;
};
if self.settings.active_level_index >= self.levels.len() {
return;
}
let stair_size = {
let layout = &self.levels[self.settings.active_level_index];
let Some(stair) = layout.stairs.get(drag.stair_idx) else {
return;
};
stair.size
};
let max_x = self.settings.cols.saturating_sub(stair_size);
let max_y = self.settings.rows.saturating_sub(stair_size);
let target_x = (grid_x - drag.offset_x).floor().max(0.0) as usize;
let target_y = (grid_y - drag.offset_y).floor().max(0.0) as usize;
let new_cell = (target_x.min(max_x), target_y.min(max_y));
if self.settings.sync_stairs_across_levels {
// Move ALL stairs that were at the original location across ALL levels.
for layout in &mut self.levels {
for stair in &mut layout.stairs {
if stair.cell == drag.original_cell {
stair.cell = new_cell;
}
}
// Ensure room coverage for each stair moved.
let stairs_to_check = layout.stairs.clone();
for stair in &stairs_to_check {
if stair.cell == new_cell {
startend::ensure_stair_in_room(layout, stair);
}
}
}
} else {
// Move only the linked stairs (current level and adjacent levels if positions match).
let current_idx = self.settings.active_level_index;
let level_indices = vec![
Some(current_idx),
current_idx.checked_sub(1),
if current_idx + 1 < self.levels.len() {
Some(current_idx + 1)
} else {
None
},
];
for idx in level_indices.into_iter().flatten() {
let layout = &mut self.levels[idx];
for stair in &mut layout.stairs {
if stair.cell == drag.original_cell {
stair.cell = new_cell;
}
}
// Ensure room coverage for each stair moved.
let stairs_to_check = layout.stairs.clone();
for stair in &stairs_to_check {
if stair.cell == new_cell {
startend::ensure_stair_in_room(layout, stair);
}
}
}
}
// Update the drag state with the new position to allow continuous dragging.
if let Some(DragState::Staircase(ref mut d)) = self.drag_state {
d.original_cell = new_cell;
}
}
// Handle right-click resizing of rooms and return whether resizing is active.
fn handle_resize(
&mut self,
@@ -1678,6 +1818,38 @@ impl DungeonApp {
}
}
if let Some(stair_idx) = self.stair_at_pointer(pointer_pos, geometry) {
let original_cell = {
let layout = &self.levels[self.settings.active_level_index];
layout.stairs[stair_idx].cell
};
self.push_undo_snapshot();
if self.settings.sync_stairs_across_levels {
// Remove ALL stairs at this location across ALL levels.
for layout in &mut self.levels {
layout.stairs.retain(|s| s.cell != original_cell);
}
} else {
// Remove linked stairs on current and adjacent levels.
let current_idx = self.settings.active_level_index;
let level_indices = vec![
Some(current_idx),
current_idx.checked_sub(1),
if current_idx + 1 < self.levels.len() {
Some(current_idx + 1)
} else {
None
},
];
for idx in level_indices.into_iter().flatten() {
self.levels[idx].stairs.retain(|s| s.cell != original_cell);
}
}
return;
}
if let Some((room_idx, _, _)) = self.room_at_pointer(pointer_pos, geometry) {
self.push_undo_snapshot();
self.delete_room(room_idx);
@@ -1931,6 +2103,15 @@ enum DragState {
Text(TextDragState),
Marker(MarkerDragState),
Corridor(CorridorDragState),
Staircase(StaircaseDragState),
}
#[derive(Debug, Clone)]
struct StaircaseDragState {
stair_idx: usize,
offset_x: f32,
offset_y: f32,
original_cell: (usize, usize),
}
#[derive(Debug, Clone)]
@@ -2011,6 +2192,7 @@ fn draw_layout(
colorblind_mode: bool,
hover_text_idx: Option<usize>,
hover_marker: Option<HoverMarker>,
hover_stair_idx: Option<usize>,
) {
let wall_width_px = (geometry.cell_size / 2.5).max(1.0);
let door_width_px = (geometry.cell_size / 10.0).max(1.0);
@@ -2305,8 +2487,14 @@ fn draw_layout(
);
// Draw stairs and elevators.
for stair in &layout.stairs {
draw_staircase(painter, geometry, stair, colorblind_mode);
for (idx, stair) in layout.stairs.iter().enumerate() {
draw_staircase(
painter,
geometry,
stair,
colorblind_mode,
hover_stair_idx == Some(idx),
);
}
}
@@ -2316,6 +2504,7 @@ fn draw_staircase(
geometry: &GridGeometry,
stair: &layout::Staircase,
colorblind_mode: bool,
is_hovered: 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;
@@ -2324,7 +2513,9 @@ fn draw_staircase(
let rect = egui::Rect::from_min_max(egui::pos2(left, top), egui::pos2(right, bottom));
// Fill with a distinctive color for stairs.
let fill = if colorblind_mode {
let fill = if is_hovered {
Color32::from_rgb(220, 180, 80).gamma_multiply(0.5)
} else if colorblind_mode {
Color32::from_rgb(180, 180, 180).gamma_multiply(0.4)
} else {
Color32::from_rgb(200, 150, 50).gamma_multiply(0.35)
@@ -2332,7 +2523,9 @@ fn draw_staircase(
painter.rect_filled(rect, 4.0, fill);
// Draw border.
let stroke_color = if colorblind_mode {
let stroke_color = if is_hovered {
Color32::WHITE
} else if colorblind_mode {
Color32::BLACK
} else {
Color32::from_rgb(200, 150, 50)
@@ -2340,7 +2533,7 @@ fn draw_staircase(
painter.rect_stroke(
rect,
4.0,
Stroke::new(2.0, stroke_color),
Stroke::new(if is_hovered { 3.0 } else { 2.0 }, stroke_color),
egui::StrokeKind::Middle,
);
+1 -1
View File
@@ -438,7 +438,7 @@ fn get_stair_count(settings: &UiSettings, gap_idx: usize) -> usize {
}
// 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) {
pub 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;
+1 -1
View File
@@ -832,7 +832,7 @@ fn draw_layout_tab(ui: &mut egui::Ui, settings: &mut UiSettings, result: &mut Si
ui.add_space(12.0);
ui.separator();
ui.add_space(8.0);
ui.label(RichText::new("Staircase Settings").strong());
ui.label(RichText::new("Staircase/Elevator Settings").strong());
ui.add_space(8.0);
result.settings_changed |= ui