terrain_forge/algorithms/
room_accretion.rs

1use crate::{Algorithm, Grid, Rng, Tile};
2use crate::semantic::{SemanticGenerator, SemanticLayers, Marker, Masks, placement};
3
4#[derive(Debug, Clone)]
5pub struct RoomAccretionConfig {
6    pub templates: Vec<RoomTemplate>,
7    pub max_rooms: usize,
8    pub loop_chance: f64,
9}
10
11#[derive(Debug, Clone)]
12pub enum RoomTemplate {
13    Rectangle { min: usize, max: usize },
14    Blob { size: usize, smoothing: usize },
15    Circle { min_radius: usize, max_radius: usize },
16}
17
18impl Default for RoomAccretionConfig {
19    fn default() -> Self {
20        Self {
21            templates: vec![
22                RoomTemplate::Rectangle { min: 5, max: 12 },
23                RoomTemplate::Blob { size: 8, smoothing: 2 },
24                RoomTemplate::Circle { min_radius: 3, max_radius: 6 },
25            ],
26            max_rooms: 15,
27            loop_chance: 0.1,
28        }
29    }
30}
31
32pub struct RoomAccretion {
33    config: RoomAccretionConfig,
34}
35
36impl RoomAccretion {
37    pub fn new(config: RoomAccretionConfig) -> Self {
38        Self { config }
39    }
40}
41
42impl Default for RoomAccretion {
43    fn default() -> Self {
44        Self::new(RoomAccretionConfig::default())
45    }
46}
47
48impl Algorithm<Tile> for RoomAccretion {
49    fn generate(&self, grid: &mut Grid<Tile>, seed: u64) {
50        let mut rng = Rng::new(seed);
51        let (w, h) = (grid.width(), grid.height());
52        
53        // Start with first room in center
54        let center_x = w / 2;
55        let center_y = h / 2;
56        let template = rng.pick(&self.config.templates).unwrap().clone();
57        place_room(grid, &template, center_x, center_y, &mut rng);
58        
59        // Add rooms by sliding until they fit adjacent to existing structure
60        for _ in 1..self.config.max_rooms {
61            let template = rng.pick(&self.config.templates).unwrap().clone();
62            
63            // Try multiple positions
64            let mut placed = false;
65            for _ in 0..50 {
66                let start_x = rng.range_usize(5, w - 5);
67                let start_y = rng.range_usize(5, h - 5);
68                
69                if let Some((final_x, final_y)) = slide_to_fit(grid, &template, start_x, start_y, &mut rng) {
70                    place_room(grid, &template, final_x, final_y, &mut rng);
71                    
72                    // Connect to existing structure
73                    connect_to_existing(grid, final_x, final_y, &template, &mut rng);
74                    placed = true;
75                    break;
76                }
77            }
78            
79            if !placed { break; }
80        }
81        
82        // Add loops
83        if self.config.loop_chance > 0.0 {
84            crate::effects::connect_regions_spanning(grid, self.config.loop_chance, &mut rng);
85        }
86    }
87
88    fn name(&self) -> &'static str {
89        "RoomAccretion"
90    }
91}
92
93impl SemanticGenerator<Tile> for RoomAccretion {
94    fn generate_semantic(&self, grid: &Grid<Tile>, rng: &mut Rng) -> SemanticLayers {
95        let mut regions = placement::extract_regions(grid);
96        
97        // Tag regions based on size and shape
98        for region in &mut regions {
99            let area = region.area();
100            if area > 30 {
101                region.kind = "room".to_string();
102                region.add_tag("accretion_room");
103                
104                // Tag by approximate template type
105                if area > 100 {
106                    region.add_tag("large_room");
107                } else if area < 50 {
108                    region.add_tag("small_room");
109                }
110            } else {
111                region.kind = "corridor".to_string();
112                region.add_tag("connector");
113            }
114        }
115        
116        // Generate diverse markers for rooms
117        let room_regions: Vec<_> = regions.iter().filter(|r| r.kind == "room").cloned().collect();
118        let mut markers = Vec::new();
119        
120        for region in &room_regions {
121            let area = region.area();
122            
123            // Loot slots proportional to room size
124            let loot_count = (area / 25).max(1).min(4);
125            for _ in 0..loot_count {
126                if let Some(&(x, y)) = region.cells.get(rng.range_usize(0, region.cells.len())) {
127                    markers.push(
128                        Marker::new(x, y, "loot_slot")
129                            .with_region(region.id)
130                    );
131                }
132            }
133            
134            // Special markers for larger rooms
135            if area > 80 {
136                if let Some(&(x, y)) = region.cells.get(rng.range_usize(0, region.cells.len())) {
137                    markers.push(
138                        Marker::new(x, y, "boss_spawn")
139                            .with_region(region.id)
140                            .with_weight(3.0)
141                    );
142                }
143            }
144            
145            // Light anchors for medium+ rooms
146            if area > 40 {
147                if let Some(&(x, y)) = region.cells.get(rng.range_usize(0, region.cells.len())) {
148                    markers.push(
149                        Marker::new(x, y, "light_anchor")
150                            .with_region(region.id)
151                    );
152                }
153            }
154        }
155        
156        let masks = Masks::from_tiles(grid);
157        let connectivity = placement::build_connectivity(grid, &regions);
158        
159        SemanticLayers {
160            regions,
161            markers,
162            masks,
163            connectivity,
164        }
165    }
166}
167
168fn place_room(grid: &mut Grid<Tile>, template: &RoomTemplate, cx: usize, cy: usize, rng: &mut Rng) {
169    match template {
170        RoomTemplate::Rectangle { min, max } => {
171            let size = rng.range_usize(*min, *max + 1);
172            let half = size / 2;
173            for y in cy.saturating_sub(half)..=(cy + half).min(grid.height() - 1) {
174                for x in cx.saturating_sub(half)..=(cx + half).min(grid.width() - 1) {
175                    grid.set(x as i32, y as i32, Tile::Floor);
176                }
177            }
178        }
179        RoomTemplate::Circle { min_radius, max_radius } => {
180            let radius = rng.range_usize(*min_radius, *max_radius + 1);
181            let r2 = (radius * radius) as f64;
182            for dy in -(radius as i32)..=(radius as i32) {
183                for dx in -(radius as i32)..=(radius as i32) {
184                    if (dx * dx + dy * dy) as f64 <= r2 {
185                        let x = (cx as i32 + dx).max(0).min(grid.width() as i32 - 1) as usize;
186                        let y = (cy as i32 + dy).max(0).min(grid.height() as i32 - 1) as usize;
187                        grid.set(x as i32, y as i32, Tile::Floor);
188                    }
189                }
190            }
191        }
192        RoomTemplate::Blob { size, smoothing } => {
193            // Create blob using cellular automata
194            let half = size / 2;
195            let mut temp_grid = Grid::new(size + 2, size + 2);
196            
197            // Random fill
198            for y in 1..size + 1 {
199                for x in 1..size + 1 {
200                    if rng.chance(0.45) {
201                        temp_grid.set(x as i32, y as i32, Tile::Floor);
202                    }
203                }
204            }
205            
206            // Smooth
207            for _ in 0..*smoothing {
208                let mut new_grid = temp_grid.clone();
209                for y in 1..size + 1 {
210                    for x in 1..size + 1 {
211                        let neighbors = [
212                            temp_grid[(x - 1, y)], temp_grid[(x + 1, y)],
213                            temp_grid[(x, y - 1)], temp_grid[(x, y + 1)],
214                            temp_grid[(x - 1, y - 1)], temp_grid[(x + 1, y - 1)],
215                            temp_grid[(x - 1, y + 1)], temp_grid[(x + 1, y + 1)],
216                        ];
217                        let floor_count = neighbors.iter().filter(|t| t.is_floor()).count();
218                        new_grid.set(x as i32, y as i32, if floor_count >= 4 { Tile::Floor } else { Tile::Wall });
219                    }
220                }
221                temp_grid = new_grid;
222            }
223            
224            // Copy to main grid
225            for y in 1..size + 1 {
226                for x in 1..size + 1 {
227                    if temp_grid[(x, y)].is_floor() {
228                        let gx = (cx as i32 + x as i32 - half as i32 - 1).max(0).min(grid.width() as i32 - 1);
229                        let gy = (cy as i32 + y as i32 - half as i32 - 1).max(0).min(grid.height() as i32 - 1);
230                        grid.set(gx, gy, Tile::Floor);
231                    }
232                }
233            }
234        }
235    }
236}
237
238fn slide_to_fit(grid: &Grid<Tile>, template: &RoomTemplate, start_x: usize, start_y: usize, rng: &mut Rng) -> Option<(usize, usize)> {
239    let directions = [(0, -1), (1, 0), (0, 1), (-1, 0)]; // N, E, S, W
240    let direction = rng.pick(&directions).unwrap();
241    
242    let mut x = start_x as i32;
243    let mut y = start_y as i32;
244    
245    // Slide until we hit existing floor or boundary
246    for _ in 0..50 {
247        if would_overlap(grid, template, x as usize, y as usize) {
248            // Back up one step and check if adjacent
249            x -= direction.0;
250            y -= direction.1;
251            if is_adjacent_to_floor(grid, template, x as usize, y as usize) {
252                return Some((x as usize, y as usize));
253            }
254            return None;
255        }
256        
257        x += direction.0;
258        y += direction.1;
259        
260        if x < 5 || y < 5 || x >= grid.width() as i32 - 5 || y >= grid.height() as i32 - 5 {
261            return None;
262        }
263    }
264    
265    None
266}
267
268fn would_overlap(grid: &Grid<Tile>, template: &RoomTemplate, cx: usize, cy: usize) -> bool {
269    let bounds = get_template_bounds(template);
270    for dy in -bounds.1..=bounds.1 {
271        for dx in -bounds.0..=bounds.0 {
272            let x = (cx as i32 + dx).max(0).min(grid.width() as i32 - 1) as usize;
273            let y = (cy as i32 + dy).max(0).min(grid.height() as i32 - 1) as usize;
274            if grid[(x, y)].is_floor() {
275                return true;
276            }
277        }
278    }
279    false
280}
281
282fn is_adjacent_to_floor(grid: &Grid<Tile>, template: &RoomTemplate, cx: usize, cy: usize) -> bool {
283    let bounds = get_template_bounds(template);
284    for dy in -(bounds.1 + 1)..=(bounds.1 + 1) {
285        for dx in -(bounds.0 + 1)..=(bounds.0 + 1) {
286            let x = (cx as i32 + dx).max(0).min(grid.width() as i32 - 1) as usize;
287            let y = (cy as i32 + dy).max(0).min(grid.height() as i32 - 1) as usize;
288            if grid[(x, y)].is_floor() {
289                return true;
290            }
291        }
292    }
293    false
294}
295
296fn get_template_bounds(template: &RoomTemplate) -> (i32, i32) {
297    match template {
298        RoomTemplate::Rectangle { max, .. } => ((*max / 2) as i32, (*max / 2) as i32),
299        RoomTemplate::Circle { max_radius, .. } => (*max_radius as i32, *max_radius as i32),
300        RoomTemplate::Blob { size, .. } => ((size / 2) as i32, (size / 2) as i32),
301    }
302}
303
304fn connect_to_existing(grid: &mut Grid<Tile>, cx: usize, cy: usize, template: &RoomTemplate, rng: &mut Rng) {
305    let bounds = get_template_bounds(template);
306    
307    // Find edge of room
308    let mut edge_points = Vec::new();
309    for dy in -bounds.1..=bounds.1 {
310        for dx in -bounds.0..=bounds.0 {
311            let x = (cx as i32 + dx).max(0).min(grid.width() as i32 - 1) as usize;
312            let y = (cy as i32 + dy).max(0).min(grid.height() as i32 - 1) as usize;
313            if grid[(x, y)].is_floor() {
314                // Check if on edge
315                let neighbors = [
316                    (x.wrapping_sub(1), y), (x + 1, y), (x, y.wrapping_sub(1)), (x, y + 1)
317                ];
318                for &(nx, ny) in &neighbors {
319                    if nx < grid.width() && ny < grid.height() && !grid[(nx, ny)].is_floor() {
320                        edge_points.push((x, y));
321                        break;
322                    }
323                }
324            }
325        }
326    }
327    
328    if let Some(&(start_x, start_y)) = rng.pick(&edge_points) {
329        // Carve a short corridor
330        let directions = [(0, -1), (1, 0), (0, 1), (-1, 0)];
331        let direction = rng.pick(&directions).unwrap();
332        
333        for i in 1..=3 {
334            let x = (start_x as i32 + direction.0 * i).max(0).min(grid.width() as i32 - 1);
335            let y = (start_y as i32 + direction.1 * i).max(0).min(grid.height() as i32 - 1);
336            grid.set(x, y, Tile::Floor);
337        }
338    }
339}