From fd574dd2cea27216864ea7fe7a8eb214e952023c Mon Sep 17 00:00:00 2001 From: grimsace Date: Thu, 21 May 2026 13:44:29 -0500 Subject: [PATCH] fixed a bug with start and end marker placement --- src/app.rs | 15 ++ src/layout/generation/markers.rs | 228 ++++++++++++++++++++++++------- 2 files changed, 197 insertions(+), 46 deletions(-) diff --git a/src/app.rs b/src/app.rs index 324f930..5ddf92f 100644 --- a/src/app.rs +++ b/src/app.rs @@ -186,6 +186,10 @@ impl eframe::App for DungeonApp { } else { self.levels .push(generate_level(&self.settings, self.levels.len())); + // Stairs first so markers can be placed away from level transitions. + let stair_results = layout::populate_stairs(self.levels.clone(), &self.settings); + self.levels = stair_results.into_iter().map(|(l, _)| l).collect(); + layout::assign_start_end_markers(&mut self.levels, &self.settings); self.refresh_stairs(); } self.settings.active_level_index = self.levels.len() - 1; @@ -410,6 +414,11 @@ pub fn generate_all_levels(settings: &UiSettings) -> Vec { for i in 0..level_count { levels.push(generate_level(settings, i)); } + // Populate stairs first so assign_start_end_markers can place start/end + // markers as far from the stairs (level transitions) as possible. + let stair_results = layout::populate_stairs(levels, settings); + let mut levels: Vec<_> = stair_results.into_iter().map(|(l, _)| l).collect(); + layout::assign_start_end_markers(&mut levels, settings); levels } @@ -604,6 +613,12 @@ impl DungeonApp { self.settings.overlay_level_index = Some(overlay_idx - 1); } } + if !self.settings.composition_mode { + // Stairs first so markers can be placed away from level transitions. + let stair_results = layout::populate_stairs(self.levels.clone(), &self.settings); + self.levels = stair_results.into_iter().map(|(l, _)| l).collect(); + layout::assign_start_end_markers(&mut self.levels, &self.settings); + } self.reset_transient_state(); } diff --git a/src/layout/generation/markers.rs b/src/layout/generation/markers.rs index 80221e4..48bb7bb 100644 --- a/src/layout/generation/markers.rs +++ b/src/layout/generation/markers.rs @@ -10,7 +10,8 @@ use crate::seed; use crate::ui::UiSettings; use std::collections::HashSet; -// Populates random markers. +// Populates per-level markers (traps and monsters only). +// Start/end markers are assigned across levels by assign_start_end_markers in app.rs. pub fn populate_random_markers(mut layout: DungeonLayout, settings: &UiSettings) -> DungeonLayout { layout.start_markers.clear(); layout.end_markers.clear(); @@ -21,6 +22,33 @@ pub fn populate_random_markers(mut layout: DungeonLayout, settings: &UiSettings) return layout; } + layout = populate_random_traps(layout, settings); + layout = populate_random_monsters(layout, settings); + layout +} + +// Distributes start/end markers across all dungeon levels. +// +// Start markers are placed on the first level, as far as possible from that +// level's stairs (so the player has to traverse the level to reach them). +// End markers are placed on the last level, as far as possible from that +// level's stairs. Extra markers beyond the first pair spread to adjacent +// levels using the same logic. +// +// When there is only one level the start is placed at the room farthest from +// the end (using the original farthest_room_pair logic). +pub fn assign_start_end_markers(layouts: &mut Vec, settings: &UiSettings) { + let num_levels = layouts.len(); + if num_levels == 0 { + return; + } + + // Clear any stale markers on all levels first. + for layout in layouts.iter_mut() { + layout.start_markers.clear(); + layout.end_markers.clear(); + } + let start_count = random_range_inclusive( settings.min_start_marker_count.max(1), settings.max_start_marker_count.max(1), @@ -31,60 +59,168 @@ pub fn populate_random_markers(mut layout: DungeonLayout, settings: &UiSettings) settings.max_end_marker_count.max(1), seed::derive_seed(settings.seed, 10_002), ); - 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(); + if num_levels == 1 { + // Single level: place start and end as far apart as possible within it. + let layout = &mut layouts[0]; + if layout.rooms.is_empty() { + return; + } + let all_rooms: Vec = (0..layout.rooms.len()).collect(); + let mut available_start = all_rooms.clone(); + let mut available_end = all_rooms.clone(); - 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, + let pair_count = start_count.min(end_count); + for pair_idx in 0..pair_count { + let (si, ei) = farthest_room_pair(&layout.rooms, &available_start, &available_end); + layout.start_markers.push(marker_in_room( + &layout.rooms[si], + settings.min_start_marker_size, + settings.max_start_marker_size, + seed::derive_seed(settings.seed, 11_000 + pair_idx as u64), + )); + layout.end_markers.push(marker_in_room( + &layout.rooms[ei], + settings.min_end_marker_size, + settings.max_end_marker_size, + seed::derive_seed(settings.seed, 12_000 + pair_idx as u64), + )); + consume_room(&mut available_start, si); + consume_room(&mut available_end, ei); + } + assign_extra_markers( + &mut layout.start_markers, + &layout.rooms, + &mut available_start, + pair_count, + start_count, settings.min_start_marker_size, settings.max_start_marker_size, - seed::derive_seed(settings.seed, 11_000 + pair_idx as u64), - )); - layout.end_markers.push(marker_in_room( - end_room, + settings.seed, + 13_000, + 14_000, + ); + assign_extra_markers( + &mut layout.end_markers, + &layout.rooms, + &mut available_end, + pair_count, + end_count, settings.min_end_marker_size, settings.max_end_marker_size, - seed::derive_seed(settings.seed, 12_000 + pair_idx as u64), - )); - consume_room(&mut available_start_rooms, start_room_idx); - consume_room(&mut available_end_rooms, end_room_idx); + settings.seed, + 15_000, + 16_000, + ); + return; } - 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, - 13_000, - 14_000, - ); - 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, - 15_000, - 16_000, - ); + // Multi-level: starts go on the first level far from its stairs, + // ends go on the last level far from its stairs. + // Extra markers spill to adjacent levels using the same rule. + for idx in 0..start_count { + let level_idx = idx.min(num_levels - 1); + let layout = &mut layouts[level_idx]; + if layout.rooms.is_empty() { + continue; + } + // Build a list of rooms sorted by distance from stairs on this level, + // farthest first. Rooms with no stair proximity get the whole list. + let mut available = rooms_farthest_from_stairs(layout); + // Remove rooms already claimed by previously placed start markers. + for placed_idx in 0..layout.start_markers.len() { + let placed_cell = layout.start_markers[placed_idx].cell; + if let Some(pos) = available.iter().position(|&r| { + let room = &layout.rooms[r]; + placed_cell.0 >= room.x && placed_cell.0 < room.x + room.width + && placed_cell.1 >= room.y && placed_cell.1 < room.y + room.height + }) { + available.remove(pos); + } + } + let room_idx = if available.is_empty() { + pick_room_index(&(0..layout.rooms.len()).collect::>(), + layout.rooms.len(), seed::derive_seed(settings.seed, 11_000 + idx as u64)) + } else { + available[0] + }; + let marker = marker_in_room( + &layout.rooms[room_idx], + settings.min_start_marker_size, + settings.max_start_marker_size, + seed::derive_seed(settings.seed, 11_500 + idx as u64), + ); + layout.start_markers.push(marker); + } - layout = populate_random_traps(layout, settings); - layout = populate_random_monsters(layout, settings); - layout + for idx in 0..end_count { + let level_idx = num_levels.saturating_sub(1 + idx.min(num_levels - 1)); + let layout = &mut layouts[level_idx]; + if layout.rooms.is_empty() { + continue; + } + let mut available = rooms_farthest_from_stairs(layout); + for placed_idx in 0..layout.end_markers.len() { + let placed_cell = layout.end_markers[placed_idx].cell; + if let Some(pos) = available.iter().position(|&r| { + let room = &layout.rooms[r]; + placed_cell.0 >= room.x && placed_cell.0 < room.x + room.width + && placed_cell.1 >= room.y && placed_cell.1 < room.y + room.height + }) { + available.remove(pos); + } + } + let room_idx = if available.is_empty() { + pick_room_index(&(0..layout.rooms.len()).collect::>(), + layout.rooms.len(), seed::derive_seed(settings.seed, 12_000 + idx as u64)) + } else { + available[0] + }; + let marker = marker_in_room( + &layout.rooms[room_idx], + settings.min_end_marker_size, + settings.max_end_marker_size, + seed::derive_seed(settings.seed, 12_500 + idx as u64), + ); + layout.end_markers.push(marker); + } +} + +// Returns room indices sorted by their minimum distance from any staircase on +// this level, farthest first. If the level has no stairs all rooms are +// returned in their natural order (the caller falls back to room-pair logic). +fn rooms_farthest_from_stairs(layout: &DungeonLayout) -> Vec { + if layout.stairs.is_empty() { + return (0..layout.rooms.len()).collect(); + } + + let mut distances: Vec<(usize, usize)> = layout + .rooms + .iter() + .enumerate() + .map(|(idx, room)| { + // Minimum squared distance from this room's center to any stair's center. + let min_dist = layout + .stairs + .iter() + .map(|stair| { + let stair_cx = stair.cell.0 * 2 + stair.width; + let stair_cy = stair.cell.1 * 2 + stair.height; + let room_cx = room.x * 2 + room.width; + let room_cy = room.y * 2 + room.height; + let dx = stair_cx.abs_diff(room_cx); + let dy = stair_cy.abs_diff(room_cy); + dx * dx + dy * dy + }) + .min() + .unwrap_or(0); + (idx, min_dist) + }) + .collect(); + + // Sort farthest first. + distances.sort_by(|a, b| b.1.cmp(&a.1)); + distances.into_iter().map(|(idx, _)| idx).collect() } // Generates range inclusive.