terrain_forge/algorithms/
bsp.rs1use crate::{Algorithm, Grid, Rng, Tile};
2use crate::semantic::{SemanticGenerator, SemanticLayers, Marker, Masks, placement};
3
4#[derive(Debug, Clone)]
5pub struct BspConfig {
6 pub min_room_size: usize,
7 pub max_depth: usize,
8 pub room_padding: usize,
9}
10
11impl Default for BspConfig {
12 fn default() -> Self {
13 Self { min_room_size: 5, max_depth: 4, room_padding: 1 }
14 }
15}
16
17pub struct Bsp {
18 config: BspConfig,
19}
20
21impl Bsp {
22 pub fn new(config: BspConfig) -> Self { Self { config } }
23}
24
25impl Default for Bsp {
26 fn default() -> Self { Self::new(BspConfig::default()) }
27}
28
29struct BspNode {
30 x: usize, y: usize, w: usize, h: usize,
31 left: Option<Box<BspNode>>,
32 right: Option<Box<BspNode>>,
33 room: Option<(usize, usize, usize, usize)>,
34}
35
36impl BspNode {
37 fn new(x: usize, y: usize, w: usize, h: usize) -> Self {
38 Self { x, y, w, h, left: None, right: None, room: None }
39 }
40
41 fn split(&mut self, rng: &mut Rng, min_size: usize, depth: usize, max_depth: usize) {
42 if depth >= max_depth { return; }
43
44 let can_h = self.h >= min_size * 2;
45 let can_v = self.w >= min_size * 2;
46 if !can_h && !can_v { return; }
47
48 let split_h = if can_h && can_v { rng.chance(0.5) } else { can_h };
49
50 if split_h {
51 let split = rng.range_usize(min_size, self.h - min_size + 1);
52 self.left = Some(Box::new(BspNode::new(self.x, self.y, self.w, split)));
53 self.right = Some(Box::new(BspNode::new(self.x, self.y + split, self.w, self.h - split)));
54 } else {
55 let split = rng.range_usize(min_size, self.w - min_size + 1);
56 self.left = Some(Box::new(BspNode::new(self.x, self.y, split, self.h)));
57 self.right = Some(Box::new(BspNode::new(self.x + split, self.y, self.w - split, self.h)));
58 }
59
60 if let Some(ref mut l) = self.left { l.split(rng, min_size, depth + 1, max_depth); }
61 if let Some(ref mut r) = self.right { r.split(rng, min_size, depth + 1, max_depth); }
62 }
63
64 fn create_rooms(&mut self, rng: &mut Rng, padding: usize) {
65 if self.left.is_some() || self.right.is_some() {
66 if let Some(ref mut l) = self.left { l.create_rooms(rng, padding); }
67 if let Some(ref mut r) = self.right { r.create_rooms(rng, padding); }
68 } else {
69 let min_w = 3.min(self.w.saturating_sub(padding * 2));
70 let min_h = 3.min(self.h.saturating_sub(padding * 2));
71 if min_w < 3 || min_h < 3 { return; }
72
73 let max_w = self.w.saturating_sub(padding * 2);
74 let max_h = self.h.saturating_sub(padding * 2);
75 let w = rng.range_usize(min_w, max_w + 1);
76 let h = rng.range_usize(min_h, max_h + 1);
77 let x = self.x + padding + rng.range_usize(0, max_w - w + 1);
78 let y = self.y + padding + rng.range_usize(0, max_h - h + 1);
79 self.room = Some((x, y, w, h));
80 }
81 }
82
83 fn get_center(&self) -> Option<(usize, usize)> {
84 if let Some((x, y, w, h)) = self.room {
85 return Some((x + w / 2, y + h / 2));
86 }
87 self.left.as_ref().and_then(|n| n.get_center())
88 .or_else(|| self.right.as_ref().and_then(|n| n.get_center()))
89 }
90
91 fn carve(&self, grid: &mut Grid<Tile>) {
92 if let Some((x, y, w, h)) = self.room {
93 grid.fill_rect(x as i32, y as i32, w, h, Tile::Floor);
94 }
95 if let (Some(ref left), Some(ref right)) = (&self.left, &self.right) {
96 left.carve(grid);
97 right.carve(grid);
98 if let (Some((lx, ly)), Some((rx, ry))) = (left.get_center(), right.get_center()) {
99 for x in lx.min(rx)..=lx.max(rx) { grid.set(x as i32, ly as i32, Tile::Floor); }
100 for y in ly.min(ry)..=ly.max(ry) { grid.set(rx as i32, y as i32, Tile::Floor); }
101 }
102 }
103 }
104}
105
106impl Algorithm<Tile> for Bsp {
107 fn generate(&self, grid: &mut Grid<Tile>, seed: u64) {
108 let mut rng = Rng::new(seed);
109 let mut root = BspNode::new(1, 1, grid.width() - 2, grid.height() - 2);
110 root.split(&mut rng, self.config.min_room_size, 0, self.config.max_depth);
111 root.create_rooms(&mut rng, self.config.room_padding);
112 root.carve(grid);
113 }
114
115 fn name(&self) -> &'static str { "BSP" }
116}
117
118impl SemanticGenerator<Tile> for Bsp {
119 fn generate_semantic(&self, grid: &Grid<Tile>, rng: &mut Rng) -> SemanticLayers {
120 let mut regions = placement::extract_regions(grid);
121
122 for region in &mut regions {
124 let area = region.area();
125 if area >= self.config.min_room_size * self.config.min_room_size {
126 region.kind = "room".to_string();
127 region.add_tag("bsp_room");
128 } else {
129 region.kind = "corridor".to_string();
130 region.add_tag("connector");
131 }
132 }
133
134 let room_regions: Vec<_> = regions.iter().filter(|r| r.kind == "room").cloned().collect();
136 let mut markers = placement::distribute_markers(&room_regions, "loot_slot", room_regions.len() * 2, rng);
137
138 for region in &room_regions {
140 if region.area() > 50 {
141 if let Some(&(x, y)) = region.cells.get(rng.range_usize(0, region.cells.len())) {
142 markers.push(
143 Marker::new(x, y, "light_anchor")
144 .with_region(region.id)
145 .with_weight(2.0)
146 );
147 }
148 }
149 }
150
151 let masks = Masks::from_tiles(grid);
152 let connectivity = placement::build_connectivity(grid, ®ions);
153
154 SemanticLayers {
155 regions,
156 markers,
157 masks,
158 connectivity,
159 }
160 }
161}