From c4a8d606e1aafff232c7b4b11e577bbc90ef73c0 Mon Sep 17 00:00:00 2001 From: grimsace Date: Mon, 27 Apr 2026 11:16:01 -0500 Subject: [PATCH] made stairs movable --- src/main.rs | 203 ++++++++++++++++++++++++++++++++++++++++++++++-- src/startend.rs | 2 +- src/ui.rs | 2 +- 3 files changed, 200 insertions(+), 7 deletions(-) diff --git a/src/main.rs b/src/main.rs index b1876cf..042c931 100644 --- a/src/main.rs +++ b/src/main.rs @@ -65,6 +65,7 @@ struct DungeonApp { hover_text_idx: Option, hover_marker: Option, hover_level_idx: Option, + hover_stair_idx: Option, } 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 { + 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 { + 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, hover_marker: Option, + hover_stair_idx: Option, ) { 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, ); diff --git a/src/startend.rs b/src/startend.rs index de12dbc..89bbe07 100644 --- a/src/startend.rs +++ b/src/startend.rs @@ -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; diff --git a/src/ui.rs b/src/ui.rs index fcd0e8a..c89a201 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -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