terrain_forge/algorithms/
room_accretion.rs1use crate::{Algorithm, Grid, Rng, Tile};
2
3#[derive(Debug, Clone)]
4pub struct RoomAccretionConfig {
5 pub templates: Vec<RoomTemplate>,
6 pub max_rooms: usize,
7 pub loop_chance: f64,
8}
9
10#[derive(Debug, Clone)]
11pub enum RoomTemplate {
12 Rectangle { min: usize, max: usize },
13 Blob { size: usize, smoothing: usize },
14 Circle { min_radius: usize, max_radius: usize },
15}
16
17impl Default for RoomAccretionConfig {
18 fn default() -> Self {
19 Self {
20 templates: vec![
21 RoomTemplate::Rectangle { min: 5, max: 12 },
22 RoomTemplate::Blob { size: 8, smoothing: 2 },
23 RoomTemplate::Circle { min_radius: 3, max_radius: 6 },
24 ],
25 max_rooms: 15,
26 loop_chance: 0.1,
27 }
28 }
29}
30
31pub struct RoomAccretion {
32 config: RoomAccretionConfig,
33}
34
35impl RoomAccretion {
36 pub fn new(config: RoomAccretionConfig) -> Self {
37 Self { config }
38 }
39}
40
41impl Default for RoomAccretion {
42 fn default() -> Self {
43 Self::new(RoomAccretionConfig::default())
44 }
45}
46
47impl Algorithm<Tile> for RoomAccretion {
48 fn generate(&self, grid: &mut Grid<Tile>, seed: u64) {
49 let mut rng = Rng::new(seed);
50 let (w, h) = (grid.width(), grid.height());
51
52 let center_x = w / 2;
54 let center_y = h / 2;
55 let template = rng.pick(&self.config.templates).unwrap().clone();
56 place_room(grid, &template, center_x, center_y, &mut rng);
57
58 for _ in 1..self.config.max_rooms {
60 let template = rng.pick(&self.config.templates).unwrap().clone();
61
62 let mut placed = false;
64 for _ in 0..50 {
65 let start_x = rng.range_usize(5, w - 5);
66 let start_y = rng.range_usize(5, h - 5);
67
68 if let Some((final_x, final_y)) = slide_to_fit(grid, &template, start_x, start_y, &mut rng) {
69 place_room(grid, &template, final_x, final_y, &mut rng);
70
71 connect_to_existing(grid, final_x, final_y, &template, &mut rng);
73 placed = true;
74 break;
75 }
76 }
77
78 if !placed { break; }
79 }
80
81 if self.config.loop_chance > 0.0 {
83 crate::effects::connect_regions_spanning(grid, self.config.loop_chance, &mut rng);
84 }
85 }
86
87 fn name(&self) -> &'static str {
88 "RoomAccretion"
89 }
90}
91
92fn place_room(grid: &mut Grid<Tile>, template: &RoomTemplate, cx: usize, cy: usize, rng: &mut Rng) {
93 match template {
94 RoomTemplate::Rectangle { min, max } => {
95 let size = rng.range_usize(*min, *max + 1);
96 let half = size / 2;
97 for y in cy.saturating_sub(half)..=(cy + half).min(grid.height() - 1) {
98 for x in cx.saturating_sub(half)..=(cx + half).min(grid.width() - 1) {
99 grid.set(x as i32, y as i32, Tile::Floor);
100 }
101 }
102 }
103 RoomTemplate::Circle { min_radius, max_radius } => {
104 let radius = rng.range_usize(*min_radius, *max_radius + 1);
105 let r2 = (radius * radius) as f64;
106 for dy in -(radius as i32)..=(radius as i32) {
107 for dx in -(radius as i32)..=(radius as i32) {
108 if (dx * dx + dy * dy) as f64 <= r2 {
109 let x = (cx as i32 + dx).max(0).min(grid.width() as i32 - 1) as usize;
110 let y = (cy as i32 + dy).max(0).min(grid.height() as i32 - 1) as usize;
111 grid.set(x as i32, y as i32, Tile::Floor);
112 }
113 }
114 }
115 }
116 RoomTemplate::Blob { size, smoothing } => {
117 let half = size / 2;
119 let mut temp_grid = Grid::new(size + 2, size + 2);
120
121 for y in 1..size + 1 {
123 for x in 1..size + 1 {
124 if rng.chance(0.45) {
125 temp_grid.set(x as i32, y as i32, Tile::Floor);
126 }
127 }
128 }
129
130 for _ in 0..*smoothing {
132 let mut new_grid = temp_grid.clone();
133 for y in 1..size + 1 {
134 for x in 1..size + 1 {
135 let neighbors = [
136 temp_grid[(x - 1, y)], temp_grid[(x + 1, y)],
137 temp_grid[(x, y - 1)], temp_grid[(x, y + 1)],
138 temp_grid[(x - 1, y - 1)], temp_grid[(x + 1, y - 1)],
139 temp_grid[(x - 1, y + 1)], temp_grid[(x + 1, y + 1)],
140 ];
141 let floor_count = neighbors.iter().filter(|t| t.is_floor()).count();
142 new_grid.set(x as i32, y as i32, if floor_count >= 4 { Tile::Floor } else { Tile::Wall });
143 }
144 }
145 temp_grid = new_grid;
146 }
147
148 for y in 1..size + 1 {
150 for x in 1..size + 1 {
151 if temp_grid[(x, y)].is_floor() {
152 let gx = (cx as i32 + x as i32 - half as i32 - 1).max(0).min(grid.width() as i32 - 1);
153 let gy = (cy as i32 + y as i32 - half as i32 - 1).max(0).min(grid.height() as i32 - 1);
154 grid.set(gx, gy, Tile::Floor);
155 }
156 }
157 }
158 }
159 }
160}
161
162fn slide_to_fit(grid: &Grid<Tile>, template: &RoomTemplate, start_x: usize, start_y: usize, rng: &mut Rng) -> Option<(usize, usize)> {
163 let directions = [(0, -1), (1, 0), (0, 1), (-1, 0)]; let direction = rng.pick(&directions).unwrap();
165
166 let mut x = start_x as i32;
167 let mut y = start_y as i32;
168
169 for _ in 0..50 {
171 if would_overlap(grid, template, x as usize, y as usize) {
172 x -= direction.0;
174 y -= direction.1;
175 if is_adjacent_to_floor(grid, template, x as usize, y as usize) {
176 return Some((x as usize, y as usize));
177 }
178 return None;
179 }
180
181 x += direction.0;
182 y += direction.1;
183
184 if x < 5 || y < 5 || x >= grid.width() as i32 - 5 || y >= grid.height() as i32 - 5 {
185 return None;
186 }
187 }
188
189 None
190}
191
192fn would_overlap(grid: &Grid<Tile>, template: &RoomTemplate, cx: usize, cy: usize) -> bool {
193 let bounds = get_template_bounds(template);
194 for dy in -bounds.1..=bounds.1 {
195 for dx in -bounds.0..=bounds.0 {
196 let x = (cx as i32 + dx).max(0).min(grid.width() as i32 - 1) as usize;
197 let y = (cy as i32 + dy).max(0).min(grid.height() as i32 - 1) as usize;
198 if grid[(x, y)].is_floor() {
199 return true;
200 }
201 }
202 }
203 false
204}
205
206fn is_adjacent_to_floor(grid: &Grid<Tile>, template: &RoomTemplate, cx: usize, cy: usize) -> bool {
207 let bounds = get_template_bounds(template);
208 for dy in -(bounds.1 + 1)..=(bounds.1 + 1) {
209 for dx in -(bounds.0 + 1)..=(bounds.0 + 1) {
210 let x = (cx as i32 + dx).max(0).min(grid.width() as i32 - 1) as usize;
211 let y = (cy as i32 + dy).max(0).min(grid.height() as i32 - 1) as usize;
212 if grid[(x, y)].is_floor() {
213 return true;
214 }
215 }
216 }
217 false
218}
219
220fn get_template_bounds(template: &RoomTemplate) -> (i32, i32) {
221 match template {
222 RoomTemplate::Rectangle { max, .. } => ((*max / 2) as i32, (*max / 2) as i32),
223 RoomTemplate::Circle { max_radius, .. } => (*max_radius as i32, *max_radius as i32),
224 RoomTemplate::Blob { size, .. } => ((size / 2) as i32, (size / 2) as i32),
225 }
226}
227
228fn connect_to_existing(grid: &mut Grid<Tile>, cx: usize, cy: usize, template: &RoomTemplate, rng: &mut Rng) {
229 let bounds = get_template_bounds(template);
230
231 let mut edge_points = Vec::new();
233 for dy in -bounds.1..=bounds.1 {
234 for dx in -bounds.0..=bounds.0 {
235 let x = (cx as i32 + dx).max(0).min(grid.width() as i32 - 1) as usize;
236 let y = (cy as i32 + dy).max(0).min(grid.height() as i32 - 1) as usize;
237 if grid[(x, y)].is_floor() {
238 let neighbors = [
240 (x.wrapping_sub(1), y), (x + 1, y), (x, y.wrapping_sub(1)), (x, y + 1)
241 ];
242 for &(nx, ny) in &neighbors {
243 if nx < grid.width() && ny < grid.height() && !grid[(nx, ny)].is_floor() {
244 edge_points.push((x, y));
245 break;
246 }
247 }
248 }
249 }
250 }
251
252 if let Some(&(start_x, start_y)) = rng.pick(&edge_points) {
253 let directions = [(0, -1), (1, 0), (0, 1), (-1, 0)];
255 let direction = rng.pick(&directions).unwrap();
256
257 for i in 1..=3 {
258 let x = (start_x as i32 + direction.0 * i).max(0).min(grid.width() as i32 - 1);
259 let y = (start_y as i32 + direction.1 * i).max(0).min(grid.height() as i32 - 1);
260 grid.set(x, y, Tile::Floor);
261 }
262 }
263}