1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
//! Collection of utility function to calculate field of vision.

use crate::{
    bresenham::{BresenhamLine, ThickBresenhamCircle},
    Point,
};

/// Implement the VisionMap trait to use the field of view function.
pub trait VisionMap {
    /// Dimension of your map, in grid size.
    fn dimensions(&self) -> (i32, i32);
    /// Wether it is possible or not to see through the tile at position `(x, y)`.
    /// Used by field of view algorithm.
    fn is_transparent(&self, position: Point) -> bool;
}

/// An implementation of the field of view algorithm using basic raycasting.
/// Returns a vector containing all points visible from the starting position, including the starting position.
///
/// Adapted the algorithm found on the [visibility determination](https://sites.google.com/site/jicenospam/visibilitydetermination).
/// For a comparison of the different raycasting types, advantages and disavantages, see
/// [roguebasin's comparison](http://www.roguebasin.com/index.php?title=Comparative_study_of_field_of_view_algorithms_for_2D_grid_based_worlds)
///
/// It differs from the original algorithm in the way it decides where to cast rays: It uses a circle around the
/// center instead of a square, which avoids the necessity of error correction.
///
/// The result is more homogeneous and faster.
///
/// # Arguments
///
/// * `map` - A struct implementing the `VisionMap` trait.
/// * `from` - The origin/center of the field of vision.
/// * `radius` - How far the vision should go. Should be higher or equal to 0 (If 0 or less, you only see yourself).
///
/// # Panics
///
/// Panics if `from` is out of the map bounds.
///
/// # Examples
/// ```
/// use torchbearer::{
///     fov::{field_of_view, VisionMap},
///     Point,
/// };
///
/// struct SampleMap {
///     width: i32,
///     height: i32,
///     transparent: Vec<bool>,
/// }
///
/// impl SampleMap {
///     fn new(width: i32, height: i32) -> Self {
///         // (…)
/// #        SampleMap {
/// #            width,
/// #            height,
/// #            transparent: vec![true; (width * height) as usize],
/// #        }
///     }
/// }
///
/// impl VisionMap for SampleMap {
///     fn dimensions(&self) -> (i32, i32) {
///         (self.width, self.height)
///     }
///
///     fn is_transparent(&self, (x, y): Point) -> bool {
///         self.transparent[(x + y * self.width) as usize]
///     }
/// }
///
/// let sample_map = SampleMap::new(16, 10);
///
/// // (…) You probably want at this point to add some walls to your map.
/// let from = (1, 1);
/// let radius = 5;
/// let visible_positions = field_of_view(&sample_map, from, radius);
///
/// for visible_position in visible_positions {
///     // (…)
/// }
/// ```
pub fn field_of_view<T: VisionMap>(map: &T, from: Point, radius: i32) -> Vec<(i32, i32)> {
    let (x, y) = from;
    assert_in_bounds(map, x, y);

    if radius < 1 {
        return vec![(x, y)];
    }

    let (width, height) = map.dimensions();

    let minx = (x - radius).max(0);
    let miny = (y - radius).max(0);
    let maxx = (x + radius).min(width - 1);
    let maxy = (y + radius).min(height - 1);

    if maxx - minx == 0 || maxy - miny == 0 {
        // Well, no area to check.
        return vec![];
    }

    let (sub_width, sub_height) = (maxx - minx + 1, maxy - miny + 1);
    let (offset_x, offset_y) = (minx, miny);

    let mut visibles = vec![false; (sub_width * sub_height) as usize];
    // Set origin as visible.
    visibles[(x - offset_x + (y - offset_y) * sub_width) as usize] = true;

    for point in ThickBresenhamCircle::new(from, radius) {
        cast_ray(
            map,
            &mut visibles,
            sub_width,
            sub_height,
            from,
            point,
            (offset_x, offset_y),
        );
    }

    visibles
        .into_iter()
        .enumerate()
        .filter_map(|(index, visible)| {
            if visible {
                Some((
                    index as i32 % sub_width + offset_x,
                    index as i32 / sub_width + offset_y,
                ))
            } else {
                None
            }
        })
        .collect()
}

fn is_out_of_bounds<M: VisionMap>(map: &M, x: i32, y: i32) -> bool {
    let (width, height) = map.dimensions();
    x < 0 || y < 0 || x >= width || y >= height
}

fn assert_in_bounds<M: VisionMap>(map: &M, x: i32, y: i32) {
    let (width, height) = map.dimensions();
    if is_out_of_bounds(map, x, y) {
        panic!(
            "(x, y) should be between (0,0) and ({}, {}), got ({}, {}).",
            width, height, x, y
        );
    }
}

fn cast_ray<T: VisionMap>(
    map: &T,
    visibles: &mut [bool],
    width: i32,
    height: i32,
    origin: Point,
    destination: Point,
    offset: (i32, i32),
) {
    // We skip the first item as it is the origin position.
    let ray = BresenhamLine::new(origin, destination).skip(1);
    for (x, y) in ray {
        let (off_x, off_y) = (x - offset.0, y - offset.1);
        if off_x < 0 || off_y < 0 || off_x >= width || off_y >= height {
            // No need to continue the ray, we are out of bounds.
            return;
        }
        visibles[(off_x + off_y * width) as usize] = true;

        if !map.is_transparent((x, y)) {
            return;
        }
    }
}

#[cfg(test)]
mod tests {
    use rand::{prelude::StdRng, Rng, SeedableRng};
    use std::fmt::Debug;

    use crate::Point;

    use super::{field_of_view, VisionMap};
    const WIDTH: i32 = 45;
    const HEIGHT: i32 = 45;
    const POSITION_X: i32 = 22;
    const POSITION_Y: i32 = 22;
    const RADIUS: i32 = 24;
    const RANDOM_WALLS: i32 = 10;

    pub struct SampleMap {
        /// Vector to store the transparent tiles.
        transparent: Vec<bool>,
        /// Vector to store the computed field of vision.
        vision: Vec<bool>,
        /// The width of the map
        width: i32,
        /// The height of the map
        height: i32,
        /// The last position where the field of view was calculated. If never calculated, initialized to (-1, -1).
        last_origin: (i32, i32),
    }

    impl VisionMap for SampleMap {
        fn dimensions(&self) -> (i32, i32) {
            (self.width, self.height)
        }

        fn is_transparent(&self, (x, y): Point) -> bool {
            let index = (x + y * self.width) as usize;
            self.transparent[index]
        }
    }

    impl SampleMap {
        pub fn new(width: i32, height: i32) -> Self {
            if width <= 0 && height <= 0 {
                panic!("Width and height should be > 0, got ({},{})", width, height);
            }
            SampleMap {
                transparent: vec![true; (width * height) as usize],
                vision: vec![false; (width * height) as usize],
                width,
                height,
                last_origin: (-1, -1),
            }
        }
        /// Flag a tile as transparent or visible.
        pub fn set_transparent(&mut self, x: i32, y: i32, is_transparent: bool) {
            self.transparent[(x + y * self.width) as usize] = is_transparent;
        }

        pub fn calculate_fov(&mut self, x: i32, y: i32, radius: i32) {
            for see in self.vision.iter_mut() {
                *see = false;
            }

            let visibles = field_of_view(self, (x, y), radius);

            for (x, y) in visibles {
                self.vision[(x + y * self.width) as usize] = true
            }
            self.last_origin = (x, y);
        }
    }

    impl Debug for SampleMap {
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
            let (width, _height) = self.dimensions();

            let last_origin_index = if self.last_origin.0 >= 0 && self.last_origin.1 >= 0 {
                Some((self.last_origin.0 + self.last_origin.1 * width) as usize)
            } else {
                None
            };

            let mut display_string = String::from("+");
            display_string.push_str("-".repeat(self.width as usize).as_str());
            display_string.push_str("+\n");
            for index in 0..self.vision.len() {
                if index % self.width as usize == 0 {
                    display_string.push('|');
                }

                let is_last_origin = if let Some(last_origin_index) = last_origin_index {
                    last_origin_index == index
                } else {
                    false
                };
                let tile = match (is_last_origin, self.transparent[index], self.vision[index]) {
                    (true, _, _) => '*',
                    (_, true, true) => ' ',
                    (_, false, true) => '□',
                    _ => '?',
                };
                display_string.push(tile);
                if index > 0 && (index + 1) % self.width as usize == 0 {
                    display_string.push_str("|\n");
                }
            }
            display_string.truncate(display_string.len() - 1);
            display_string.push('\n');
            display_string.push('+');
            display_string.push_str("-".repeat(self.width as usize).as_str());
            display_string.push('+');

            write!(f, "{}", display_string)
        }
    }

    #[test]
    fn fov_with_sample_map() {
        let mut fov = SampleMap::new(10, 10);
        for x in 1..10 {
            fov.set_transparent(x, 3, false);
        }
        for y in 0..10 {
            fov.set_transparent(9, y, false);
        }
        fov.calculate_fov(3, 2, 10);

        println!("{:?}", fov);
    }

    #[test]
    fn fov_to_vector() {
        let mut fov = SampleMap::new(WIDTH, HEIGHT);

        fov.calculate_fov(POSITION_X, POSITION_Y, RADIUS);
    }

    #[test]
    fn fov_with_wall_to_vector() {
        let mut fov = SampleMap::new(WIDTH, HEIGHT);
        let mut rng = StdRng::seed_from_u64(42);
        for _ in 0..RANDOM_WALLS {
            let (x, y) = (rng.gen_range(0..WIDTH), rng.gen_range(0..HEIGHT));
            fov.set_transparent(x, y, false);
        }
        fov.set_transparent(POSITION_X, POSITION_Y, true);

        fov.calculate_fov(POSITION_X, POSITION_Y, RADIUS);

        println!("{:?}", fov);
    }

    #[test]
    #[should_panic(expected = "(x, y) should be between (0,0) and (45, 45), got (46, 46).")]
    fn fov_origin_out_of_bounds_panics() {
        let mut map = SampleMap::new(WIDTH, HEIGHT);
        let (x, y) = (WIDTH + 1, HEIGHT + 1);

        map.calculate_fov(x, y, 2);
    }
}