1use crate::nature::seeding::Cells;
8use crate::terrain::{CellType, HeightGrid};
9
10pub const ROTATION_STEP: f32 = 90.0;
13pub const HEIGHT_TOLERANCE: f32 = 1000.0;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
19pub struct Rect {
20 pub x0: i32,
22 pub y0: i32,
24 pub x1: i32,
26 pub y1: i32,
28}
29
30impl Rect {
31 pub fn width(&self) -> i32 {
33 self.x1 - self.x0 + 1
34 }
35 pub fn height(&self) -> i32 {
37 self.y1 - self.y0 + 1
38 }
39}
40
41pub fn footprint(
47 map_size: i32,
48 cell: (i32, i32),
49 width: i32,
50 height: i32,
51 rotation: i32,
52) -> Option<Rect> {
53 let (w, h) = if rotation == 1 || rotation == 3 {
54 (height, width)
55 } else {
56 (width, height)
57 };
58 let span = |c: i32, s: i32| {
59 if s & 1 == 0 {
60 (c - (s >> 1) + 1, (s >> 1) + c)
61 } else {
62 let half = (s - 1) >> 1;
63 (c - half, half + c)
64 }
65 };
66 let (x0, x1) = span(cell.0, w);
67 let (y0, y1) = span(cell.1, h);
68 let n = map_size;
69 if x0 < 0 || n <= x0 || y0 < 0 || n <= y0 || x0 < 1 || n <= x1 || y1 < 0 || n <= y1 {
70 return None;
71 }
72 Some(Rect { x0, y0, x1, y1 })
73}
74
75pub fn footprint_index(i: i32, j: i32, w: i32, h: i32, degrees: i32) -> usize {
80 (match degrees {
81 90 => i * (h + 1) + j,
82 180 => j * (w + 1) + (w - 1 - i),
83 270 => (w - 1 - i) * (h + 1) + (h - 1 - j),
84 _ => (h - 1 - j) * (w + 1) + i,
85 }) as usize
86}
87
88#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
90pub struct PlacementRules {
91 pub mask: u32,
94 pub masks: Vec<u32>,
97 pub mask_chars: String,
99 pub footprint: String,
102 pub width: i32,
105 pub height: i32,
107 pub cell_type: u8,
110}
111
112#[allow(clippy::too_many_arguments)]
119pub fn cell_admits(
120 cells: &Cells,
121 terrain: &HeightGrid,
122 x: i32,
123 y: i32,
124 mask: u32,
125 min_normal: f32,
126 tolerance: f32,
127 reference: f32,
128 override_occupant: bool,
129 override_flag: bool,
130 owner_ok: impl Fn(usize) -> bool,
131) -> bool {
132 let n = cells.size as i32;
133 if x < 0 || n <= x || y < 0 || n <= y {
134 return false;
135 }
136 let i = (n * y + x) as usize;
137 if !mask & (1u32 << (cells.types[i].0 & 0x1f)) != 0 {
138 return false;
139 }
140 let occupant = cells.occupants[i];
141 if !(occupant.is_none_or(&owner_ok) || override_occupant) {
142 return false;
143 }
144 if !(override_flag || cells.flags[i] == 0) {
145 return false;
146 }
147 let half = (n >> 1) as f32 - 0.5;
148 let (px, py) = ((i as i32 % n) as f32 - half, (i as i32 / n) as f32 - half);
149 let mut normal = [0.0f32; 3];
150 let h = cells
151 .floors
152 .ground_normal(cells.size, terrain, px, py, &mut normal);
153 min_normal <= normal[2] && (h - reference).abs() <= tolerance
154}
155
156pub fn can_place(
161 cells: &Cells,
162 terrain: &HeightGrid,
163 rules: &PlacementRules,
164 rect: Rect,
165 degrees: i32,
166 owner_ok: impl Fn(usize) -> bool,
167) -> bool {
168 (rect.y0..=rect.y1).all(|y| {
169 (rect.x0..=rect.x1)
170 .all(|x| footprint_cell(cells, terrain, rules, rect, degrees, x, y, &owner_ok).0)
171 })
172}
173
174#[allow(clippy::too_many_arguments)]
182pub fn footprint_cell(
183 cells: &Cells,
184 terrain: &HeightGrid,
185 rules: &PlacementRules,
186 rect: Rect,
187 degrees: i32,
188 x: i32,
189 y: i32,
190 owner_ok: impl Fn(usize) -> bool,
191) -> (bool, bool) {
192 let (w, h) = (rect.width(), rect.height());
193 let k = footprint_index(x - rect.x0, y - rect.y0, w, h, degrees);
194 let untouched = rules.footprint.as_bytes().get(k) == Some(&b'.');
195 let mut mask = rules.mask;
196 if !rules.masks.is_empty() {
197 if let Some(&c) = rules.mask_chars.as_bytes().get(k) {
198 if let Some(&m) = rules.masks.get((c as i32 - 0x30) as usize) {
199 mask = m;
200 }
201 }
202 }
203 let admits = cell_admits(
204 cells,
205 terrain,
206 x,
207 y,
208 mask,
209 0.0,
210 HEIGHT_TOLERANCE,
211 0.0,
212 false,
213 untouched,
214 owner_ok,
215 );
216 (admits, untouched)
217}
218
219pub fn stamp(cells: &mut Cells, rules: &PlacementRules, rect: Rect, degrees: i32, entity: usize) {
225 let n = cells.size as i32;
226 let (w, h) = (rect.width(), rect.height());
227 let fp = rules.footprint.as_bytes();
228 for y in rect.y0..=rect.y1 {
229 for x in rect.x0..=rect.x1 {
230 let i = (n * y + x) as usize;
231 let (set, owned, ty) = if fp.is_empty() {
232 (true, true, rules.cell_type)
233 } else {
234 let c = fp
235 .get(footprint_index(x - rect.x0, y - rect.y0, w, h, degrees))
236 .copied()
237 .unwrap_or(b'.');
238 (
239 c == b'#' || c == b'_',
240 c != b'.',
241 if c == b'_' { 2 } else { rules.cell_type },
242 )
243 };
244 if set {
245 cells.set_type(i, CellType(ty));
246 }
247 cells.objects[i] = owned.then_some(entity);
248 }
249 }
250}
251
252pub fn zone(cells: &mut Cells, rules: &PlacementRules, rect: Rect, degrees: i32, entity: usize) {
259 let n = cells.size as i32;
260 let (w, h) = (rect.width(), rect.height());
261 let fp = rules.footprint.as_bytes();
262 for y in rect.y0..=rect.y1 {
263 for x in rect.x0..=rect.x1 {
264 let i = (n * y + x) as usize;
265 let untouched = !fp.is_empty()
266 && fp
267 .get(footprint_index(x - rect.x0, y - rect.y0, w, h, degrees))
268 .copied()
269 .unwrap_or(b'.')
270 == b'.';
271 if untouched {
272 cells.occupants[i] = None;
273 cells.flags[i] = cells.flags[i].wrapping_add(1);
274 } else {
275 cells.occupants[i] = Some(entity);
276 }
277 }
278 }
279}
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284
285 #[test]
286 fn footprints_centre_on_the_cursor() {
287 let r = footprint(256, (100, 100), 8, 5, 0).unwrap();
289 assert_eq!((r.x0, r.x1, r.y0, r.y1), (97, 104, 98, 102));
290 let r = footprint(256, (100, 100), 8, 5, 1).unwrap();
291 assert_eq!((r.width(), r.height()), (5, 8));
292 assert!(footprint(256, (0, 100), 3, 3, 0).is_none());
293 }
294
295 #[test]
296 fn rotated_indices_cover_the_string() {
297 let (w, h) = (2, 2);
299 let mut seen: Vec<usize> = Vec::new();
300 for deg in [0, 90, 180, 270] {
301 seen.clear();
302 for j in 0..h {
303 for i in 0..w {
304 seen.push(footprint_index(i, j, w, h, deg));
305 }
306 }
307 seen.sort();
308 assert_eq!(seen, vec![0, 1, 3, 4], "{deg}");
309 }
310 }
311}