/* * Door and window placement logic. * Handles the automatic generation and arrangement of doors and windows * between rooms and corridors. */ use super::super::types::{ Door, DoorSettings, DungeonLayout, Room, Window, WindowSettings, WindowSide, }; use super::super::utils::{ SimpleRng, corridor_cells, normalized_cell_edge, room_index_at_cell, shared_opening_width, }; use super::connections::{room_collision_edges, room_exit_edge, shared_room_boundaries}; use crate::seed; use std::collections::HashSet; pub fn apply_doors( layout: &mut DungeonLayout, seed: u64, settings: DoorSettings, cols: usize, rows: usize, ) { layout.doors.clear(); if layout.packed_rooms { apply_packed_room_doors(layout, seed, settings); return; } let mut rng = SimpleRng::new(seed::derive_seed(seed, 0xD005_5EED_u64)); let base = (settings.frequency_percent.min(100) as f32) / 100.0; let room_hall = (settings.room_hallway_percent.min(100) as f32) / 100.0; let locked = (settings.locked_percent.min(100) as f32) / 100.0; let corridor_cells = corridor_cells(layout, cols, rows); let mut seen_edges = HashSet::new(); let door_chance = base * room_hall; for corridor in &layout.corridors { if corridor.path.len() < 2 { continue; } let start_room = &layout.rooms[corridor.start_room_id]; let end_room = &layout.rooms[corridor.end_room_id]; if let Some(edge) = room_exit_edge(&corridor.path, start_room, true) { if seen_edges.insert(edge) { let place_door = door_chance > 0.0 && rng.next_f32() <= door_chance; layout.doors.push(Door { from: edge.0, to: edge.1, width: corridor.width.max(1), span_width: true, locked: place_door && rng.next_f32() <= locked, archway: !place_door, secret: false, manual: false, }); } } if let Some(edge) = room_exit_edge(&corridor.path, end_room, false) { if seen_edges.insert(edge) { let place_door = door_chance > 0.0 && rng.next_f32() <= door_chance; layout.doors.push(Door { from: edge.0, to: edge.1, width: corridor.width.max(1), span_width: true, locked: place_door && rng.next_f32() <= locked, archway: !place_door, secret: false, manual: false, }); } } for (room_idx, room) in layout.rooms.iter().enumerate() { if room_idx == corridor.start_room_id || room_idx == corridor.end_room_id { continue; } for edge in room_collision_edges(&corridor.path, room) { if seen_edges.insert(edge) { let place_door = door_chance > 0.0 && rng.next_f32() <= door_chance; layout.doors.push(Door { from: edge.0, to: edge.1, width: corridor.width.max(1), span_width: true, locked: place_door && rng.next_f32() <= locked, archway: !place_door, secret: false, manual: false, }); } } } } if settings.allow_middle_corridor_doors { for &(x, y) in &corridor_cells { let right = (x + 1, y); let bottom = (x, y + 1); if x + 1 < cols && corridor_cells.contains(&right) { let edge = normalized_cell_edge((x, y), right); if seen_edges.insert(edge) && base > 0.0 && rng.next_f32() <= base { layout.doors.push(Door { from: edge.0, to: edge.1, width: 1, span_width: false, locked: rng.next_f32() <= locked, archway: false, secret: false, manual: false, }); } } if y + 1 < rows && corridor_cells.contains(&bottom) { let edge = normalized_cell_edge((x, y), bottom); if seen_edges.insert(edge) && base > 0.0 && rng.next_f32() <= base { layout.doors.push(Door { from: edge.0, to: edge.1, width: 1, span_width: false, locked: rng.next_f32() <= locked, archway: false, secret: false, manual: false, }); } } } } apply_secret_doors(layout, seed, settings); } pub fn apply_packed_room_doors(layout: &mut DungeonLayout, seed: u64, settings: DoorSettings) { let mut rng = SimpleRng::new(seed::derive_seed(seed, 0xD005_5EED_u64)); let door_chance = ((settings.frequency_percent.min(100) as f32) / 100.0) * ((settings.room_hallway_percent.min(100) as f32) / 100.0); let locked = (settings.locked_percent.min(100) as f32) / 100.0; for (a_idx, b_idx, shared_edges) in shared_room_boundaries(&layout.rooms) { let edge_idx = if shared_edges.len() <= 1 { 0 } else { rng.range_inclusive(0, shared_edges.len() - 1) }; let edge = shared_edges[edge_idx]; let place_door = door_chance > 0.0 && rng.next_f32() <= door_chance; let width = shared_opening_width( &layout.rooms[a_idx], &layout.rooms[b_idx], shared_edges.len(), 1, ); layout.doors.push(Door { from: edge.0, to: edge.1, width, span_width: width > 1, locked: place_door && rng.next_f32() <= locked, archway: !place_door, secret: false, manual: false, }); } apply_secret_doors(layout, seed, settings); } pub fn apply_secret_doors(layout: &mut DungeonLayout, seed: u64, settings: DoorSettings) { let secret_percent = settings.secret_percent.min(100); let chance = (secret_percent as f32) / 100.0; if chance <= 0.0 || layout.rooms.is_empty() || layout.doors.is_empty() { return; } let mut rng = SimpleRng::new(seed::derive_seed(seed, 0x5EC2_E700_u64)); let room_door_map = room_to_door_indices(layout); let eligible_secret_doors = (0..layout.doors.len()) .filter(|&door_idx| { let door = &layout.doors[door_idx]; !door.archway && !door.locked && door_is_secret_eligible_for_rooms(layout, &room_door_map, door_idx) }) .collect::>(); for (door_idx, door) in layout.doors.iter_mut().enumerate() { if !eligible_secret_doors.contains(&door_idx) { continue; } if rng.next_f32() <= chance { door.locked = false; door.archway = false; door.secret = true; } } } pub fn door_is_secret_eligible_for_rooms( layout: &DungeonLayout, room_door_map: &[Vec], door_idx: usize, ) -> bool { for door_indices in room_door_map { if !door_indices.contains(&door_idx) { continue; } for &other_door_idx in door_indices { if other_door_idx == door_idx { continue; } if !door_directly_connects_rooms(layout, other_door_idx) { return false; } } } true } pub fn room_to_door_indices(layout: &DungeonLayout) -> Vec> { let mut room_door_map = vec![Vec::new(); layout.rooms.len()]; for (door_idx, door) in layout.doors.iter().enumerate() { for (room_idx, room) in layout.rooms.iter().enumerate() { if door_touches_room(door, room) { room_door_map[room_idx].push(door_idx); } } } room_door_map } pub fn secret_room_ids(layout: &DungeonLayout) -> HashSet { let mut secret_rooms = HashSet::new(); for door in layout.doors.iter().filter(|door| door.secret) { for (room_idx, room) in layout.rooms.iter().enumerate() { if door_touches_room(door, room) { secret_rooms.insert(room_idx); } } } secret_rooms } pub fn door_touches_room(door: &Door, room: &Room) -> bool { cell_in_room(door.from, room) || cell_in_room(door.to, room) } pub fn door_directly_connects_rooms(layout: &DungeonLayout, door_idx: usize) -> bool { let Some(door) = layout.doors.get(door_idx) else { return false; }; let touching_rooms = layout .rooms .iter() .enumerate() .filter_map(|(room_idx, room)| door_touches_room(door, room).then_some(room_idx)) .collect::>(); touching_rooms.len() >= 2 } pub fn cell_in_room(cell: (usize, usize), room: &Room) -> bool { cell.0 >= room.x && cell.0 < room.x + room.width && cell.1 >= room.y && cell.1 < room.y + room.height } pub fn apply_windows( layout: &mut DungeonLayout, seed: u64, settings: WindowSettings, cols: usize, rows: usize, ) { layout.windows.clear(); if !settings.enabled || cols == 0 || rows == 0 { return; } let mut min_width = settings.min_width.clamp(1, 5); let max_width = settings.max_width.clamp(min_width, 5); min_width = min_width.min(max_width); let mut rng = SimpleRng::new(seed::derive_seed(seed, 0xA117_0055_u64)); let base = (settings.frequency_percent.min(100) as f32) / 100.0; let internal_ratio = (settings.room_hallway_percent.min(100) as f32) / 100.0; for segment in collect_window_segments(layout, cols, rows, settings.allow_internal_windows) { let chance = if segment.internal { if !settings.allow_internal_windows { continue; } base * internal_ratio } else { base * (1.0 - internal_ratio) }; if chance <= 0.0 || rng.next_f32() > chance { continue; } let max_segment_width = max_width.min(segment.cells.len()).max(1); let width = rng.range_inclusive(min_width.min(max_segment_width), max_segment_width); let cell = select_segment_cell(&segment.cells, width, &mut rng); layout.windows.push(Window { cell, side: segment.side, width, span_width: width > 1, }); } } #[derive(Debug, Clone)] pub struct WindowSegment { pub cells: Vec<(usize, usize)>, pub side: WindowSide, pub internal: bool, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum WindowTarget { Exterior, Corridor, Room(usize), } pub fn collect_window_segments( layout: &DungeonLayout, cols: usize, rows: usize, allow_internal_windows: bool, ) -> Vec { let corridor_cells = corridor_cells(layout, cols, rows); let secret_rooms = secret_room_ids(layout); let mut segments = Vec::new(); for (room_idx, room) in layout.rooms.iter().enumerate() { if secret_rooms.contains(&room_idx) { continue; } collect_room_side_segments( room_idx, room, layout, &corridor_cells, cols, rows, allow_internal_windows, WindowSide::Left, &mut segments, ); collect_room_side_segments( room_idx, room, layout, &corridor_cells, cols, rows, allow_internal_windows, WindowSide::Right, &mut segments, ); collect_room_side_segments( room_idx, room, layout, &corridor_cells, cols, rows, allow_internal_windows, WindowSide::Top, &mut segments, ); collect_room_side_segments( room_idx, room, layout, &corridor_cells, cols, rows, allow_internal_windows, WindowSide::Bottom, &mut segments, ); } segments } pub fn collect_room_side_segments( room_idx: usize, room: &Room, layout: &DungeonLayout, corridor_cells: &HashSet<(usize, usize)>, cols: usize, rows: usize, allow_internal_windows: bool, side: WindowSide, segments: &mut Vec, ) { let cells: Vec<(usize, usize)> = match side { WindowSide::Left => (room.y..(room.y + room.height)) .map(|y| (room.x, y)) .collect(), WindowSide::Right => (room.y..(room.y + room.height)) .map(|y| (room.x + room.width - 1, y)) .collect(), WindowSide::Top => (room.x..(room.x + room.width)) .map(|x| (x, room.y)) .collect(), WindowSide::Bottom => (room.x..(room.x + room.width)) .map(|x| (x, room.y + room.height - 1)) .collect(), }; let mut current_target = None; let mut current_cells = Vec::new(); for cell in cells { let Some(neighbor) = outward_neighbor(cell, side, cols, rows) else { push_window_segment(current_target, side, &mut current_cells, segments); current_target = Some(WindowTarget::Exterior); current_cells.push(cell); continue; }; let target = if let Some(other_room_idx) = room_index_at_cell(&layout.rooms, neighbor) { if other_room_idx == room_idx || !allow_internal_windows || other_room_idx < room_idx { None } else { Some(WindowTarget::Room(other_room_idx)) } } else if corridor_cells.contains(&neighbor) { allow_internal_windows.then_some(WindowTarget::Corridor) } else { Some(WindowTarget::Exterior) }; if current_target != target { push_window_segment(current_target, side, &mut current_cells, segments); current_target = target; } if target.is_some() { current_cells.push(cell); } } push_window_segment(current_target, side, &mut current_cells, segments); } pub fn push_window_segment( target: Option, side: WindowSide, cells: &mut Vec<(usize, usize)>, segments: &mut Vec, ) { let Some(target) = target else { cells.clear(); return; }; if cells.is_empty() { return; } segments.push(WindowSegment { cells: std::mem::take(cells), side, internal: !matches!(target, WindowTarget::Exterior), }); } pub fn outward_neighbor( cell: (usize, usize), side: WindowSide, cols: usize, rows: usize, ) -> Option<(usize, usize)> { match side { WindowSide::Left => cell.0.checked_sub(1).map(|x| (x, cell.1)), WindowSide::Right => (cell.0 + 1 < cols).then_some((cell.0 + 1, cell.1)), WindowSide::Top => cell.1.checked_sub(1).map(|y| (cell.0, y)), WindowSide::Bottom => (cell.1 + 1 < rows).then_some((cell.0, cell.1 + 1)), } } pub fn select_segment_cell( cells: &[(usize, usize)], width: usize, rng: &mut SimpleRng, ) -> (usize, usize) { let width = width.max(1); let min_offset = -((width as isize - 1) / 2); let max_offset = width as isize / 2; let low = (-min_offset) as usize; let high = cells.len().saturating_sub(1 + max_offset.max(0) as usize); let idx = if low <= high { rng.range_inclusive(low, high) } else { cells.len() / 2 }; cells[idx] }