bevy_flowfield_tiles_plugin 0.15.0

An implementation of FlowField (vector field) pathfinding as a plugin to the Bevy game engine
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
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
//! An `IntegrationField` is an array of 32-bit values. It uses the `CostField` to produce a cumulative cost to reach the end goal/target. The first 16-bits of each field cell value are used for a cost measurement while the second 16-bits are used as flags to indicate certain properties of a cell.
//!
//! When a new route needs to be processed the first 16-bits of the field values are set to `u16::MAX` and the field cell containing the goal is set to `0`. Any cells which are impassable in the `CostField` are marked in the `IntegrationField` with their second 16-bits as `INT_BITS_IMPASSABLE`.
//!
//! In order to reduce needless pathfinding near the goal a Line Of Sight (LOS) pass is performed from the goal Sector. The idea being that if an actor moves into a field cell that has LOS then it no longer needs to follow the FlowFields and can instead directly path to the goal.
//!
//! The LOS phase begins as a wavefront from the goal that interrogates the adjacent neighbouring field cells. If an adjacent cell is not marked as impassable then it must have LOS to the goal and the value of the cell receives a wavefront cost plus the LOS bit flag. The wavefront then expands (whereby the wavefront cost increments by 1) to interrogate the adjacent cells of the neighbours and repeats until the wavefront cannot propagate any further.
//!
//! As the wavefront expands it may encounter an impassable field cell. This causes two things to happen:
//!
//! First, wavefront expansion cannot continue in the direction of the impassable field cell so it is removed from being a candidate in the next round of wavefront propagation.
//!
//!Second, if there is a vacant field cell next to the impassable field cell then this indicates a Corner. A Corner means that LOS will be blocked in a given direction and the Corner is recorded for the integrated cost calculation.
//!
//! By taking a vector from the starting goal to the corner we can then extend this vector to calculate what field cells lie along a line. The field cells on this line are updated with the flag for `INT_BITS_WAVE_BLOCKED`. Meaning that as LOS expands and propagates if a WavefrontBlocked cell is encountered then the cell is removed as a candidate in further LOS propagation. This ensures that LOS cannot flow around impassable areas.
//!
//! Once the wavefront has exhausted expansion from either hitting the sector boundaries or from impassable cells/corners we can then calculate the actual integrated cost of the field.
//!
//! From the Corners of an `IntegrationField` recorded previously we start a new series of wavefronts that radiate from the corners considering any adjacent field cells that have not been marked as LOS or impassable.
//!
//! To calculate the cost of the cells in the field:
//!
//! 1. The valid cardinal neighbours of the corners are determined (one, none or many of North, East, South, West)
//! 2. For each neighbour field cell lookup their [CostField] value
//! 3. 3. Add the [CostField] cost to the [IntegrationField] cost of the current cell, this is the integrated-cost
//! 4. Wavefront propagates to the next neighbours, find their cardinals and repeat adding their cost value to to the current cells integration cost to produce their cumulative integration-cost, and repeat until the entire field is done
//!
//! The end result effectively produces a gradient of high numbers to low numbers, a flow of sorts.
//!
//! For Sectors other than the goal the process is effectively the same where boundary portals are treated as corners and wave propagation expanded.
//!

use bevy::reflect::Reflect;

use crate::flowfields::{
	fields::{Field, FieldCell, cost_field::CostField},
	route::RouteStep,
	utilities::{CompassDir, FIELD_RESOLUTION},
};

/// Flags a 'FieldCell' as having Line Of Sight
pub const INT_BITS_LOS: u32 = 0b0000_0000_0000_0001_0000_0000_0000_0000;
/// Flags a 'FieldCell' as being the goal
pub const INT_BITS_GOAL: u32 = 0b0000_0000_0000_0010_0000_0000_0000_0000;
/// Flags a 'FieldCell' to prevent wavefront propagation
pub const INT_BITS_WAVE_BLOCKED: u32 = 0b0000_0000_0000_0100_0000_0000_0000_0000;
/// Flags a 'FieldCell' as a portal
pub const INT_BITS_PORTAL: u32 = 0b0000_0000_0000_1000_0000_0000_0000_0000;
/// Flags a 'FieldCell' as being impassable
pub const INT_BITS_IMPASSABLE: u32 = 0b0000_0010_0000_0000_0000_0000_0000_0000;
/// Flags a 'FieldCell' as being a corner which is used for integrated cost propagation
pub const INT_BITS_CORNER: u32 = 0b0000_0100_0000_0000_0000_0000_0000_0000;
/// Helper for analysing the integrated cost of a 'FieldCell'
pub const INT_FILTER_BITS_COST: u32 = 0b0000_0000_0000_0000_1111_1111_1111_1111;
/// Helper for analysing which flags have been set on a 'FieldCell'
pub const INT_FILTER_BITS_FLAGS: u32 = 0b1111_1111_1111_1111_0000_0000_0000_0000;

/// The [IntegrationField] consists of integrated-cost values and markers that
/// describe a gradient/flow
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[derive(Clone, Reflect)]
pub struct IntegrationField {
	/// Integration array
	#[cfg_attr(feature = "serde", serde(with = "serde_big_array::BigArray"))]
	field: [u32; FIELD_RESOLUTION * FIELD_RESOLUTION],
	/// A list of [FieldCell] indices which are used for the integrated cost
	/// calculation of the field. In the final goal sector `los_corners` are
	/// calculated from a Line-of-Sight pass. For other sectors the portals
	/// are used as corners. When the field is being calculated it is these
	/// corners that are used to start wavefront propagation
	los_corners: Vec<usize>,
}

impl Default for IntegrationField {
	fn default() -> Self {
		IntegrationField {
			field: [u16::MAX as u32; FIELD_RESOLUTION * FIELD_RESOLUTION],
			los_corners: Vec::default(),
		}
	}
}

impl Field<u32> for IntegrationField {
	/// Get a reference to the field array
	fn get(&self) -> &[u32; FIELD_RESOLUTION * FIELD_RESOLUTION] {
		&self.field
	}
	/// Retrieve a field cell value
	///
	/// NB: This will panic if out of bounds
	fn get_field_cell_value(&self, field_cell: FieldCell) -> u32 {
		let index = field_cell.as_1d_index();
		self.field[index]
	}
	/// Set a field cell to a value
	///
	/// NB: This will panic if out of bounds
	fn set_field_cell_value(&mut self, value: u32, field_cell: FieldCell) {
		let index = field_cell.as_1d_index();
		self.field[index] = value;
	}
}

impl IntegrationField {
	/// Init [IntegrationField] with impassable/walls marked and goal values set
	pub fn init(scaled_costfield: &CostField, route_step: &RouteStep) -> Self {
		let mut field = IntegrationField::default();
		// mark walls
		for (i, value) in scaled_costfield.get().iter().enumerate() {
			if *value == 255 {
				field.field[i] = 65535 + INT_BITS_IMPASSABLE;
			}
		}
		// set goal values
		field.set_goal_value(route_step);
		//TODO consider if useful to expand LOS propagation to other sectors
		// line of sight pass on final goal sector
		if route_step.portal().is_none() {
			let wavefront_cost = 1;
			propagate_los(
				&mut field,
				&[route_step.get_goal()],
				wavefront_cost,
				route_step.get_goal(),
			);
		}
		field
	}
	/// Mark the goal in the field
	fn set_goal_value(&mut self, route_step: &RouteStep) {
		if let Some(window) = route_step.portal() {
			// mark the portal window cells are goals
			let indices = window.get_all_window_cells();
			for i in indices.iter() {
				self.field[*i] = INT_BITS_PORTAL;
				// cells other than the last need LOS corners setting as the portal cells
				self.los_corners.push(*i);
			}
		} else {
			let goal_index = route_step.get_goal();
			self.field[goal_index] = INT_BITS_GOAL;
		}
	}
	/// Perform the integrated cost calculation to build the [IntegrationField],
	/// beginning with the 'los_corners'
	pub fn build(&mut self, scaled_costfield: &CostField) {
		// list of active wavefront, element 0 is the cell, element 1 is the integrated cost
		let mut wavefront = vec![];
		for goal in self.los_corners.iter() {
			wavefront.push((
				(*goal),
				self.get_field_cell_value(FieldCell::from_index(*goal)),
			));
		}
		propagate_integrated_wavefront(self, scaled_costfield, wavefront);
	}
}

//TODO this is a diamond shaped propagation, doesn't really matter for LOS but would a spherical propagation be better? (spherical is solving the Eikonal PDE)
/// Recursively expand a wavefront and mark cells as Line-of-Sight if they have a clear path to the goal. If an impassable wall if located then test for a LOS corner and mark cells to block wavefront propagation
fn propagate_los(
	field: &mut IntegrationField,
	wavefront: &[usize],
	mut wavefront_cost: u32,
	goal: usize,
) {
	let goal_cell = FieldCell::from_index(goal);
	let mut next_wavefront = vec![];
	for cell_index in wavefront.iter() {
		// get the neighbours of the cell
		let wave_cell = FieldCell::from_index(*cell_index);
		let neighbours = wave_cell.get_orthogonal_neighbours();
		for neighbour in neighbours.iter() {
			let n_index = neighbour.as_1d_index();
			let cost = field.field[n_index];
			if cost & INT_BITS_WAVE_BLOCKED == INT_BITS_WAVE_BLOCKED
				|| cost & INT_BITS_GOAL == INT_BITS_GOAL
			{
				// wave blocked don't propagate LOS from this neighbour
			} else if cost & INT_BITS_IMPASSABLE == INT_BITS_IMPASSABLE {
				// found wall, look for LOS corner a wavefront ahead
				// based on the direction towards `n_index`, look at it's neighbours,
				// if a neighbour isn't a wall then it means there's
				// a LOS corner
				let dir = wave_cell.dir_from_this_to_rhs(neighbour);

				match dir {
					CompassDir::North | CompassDir::South => {
						// check if the corner is actually reachable from the wavefront cell
						// this prevents stepping between two diagonal wall cells and assigning
						// an incorrect wavefront flag to a corner that shouldn't exist.
						// E.g
						//  _________
						// | c A ?   |
						// |   w B   |
						// |_________|
						//
						// Wavefront in cell `w`. Neighbours `A` and `B` are walls. `A` is
						// inspected and it's a wall. We must ensure `?` isn't labelled as a
						// corner as it's blocked off diagonally by the walls. To do this we look
						// at the East and West neighbours of `w` to see if any of those are
						// walls. In this case there is a wall at `B` - meaning `?` cannot be a
						// valid corner as it's inaccessible. When the westerly neighbour of `w`
						// is inspected we find no wall, this means the empty cell at `c` must
						// be a corner and can be used for integrated cost calculation
						//
						if let Some(wave_west) =
							wave_cell.get_in_compass_direction(&CompassDir::West, 1)
						{
							let wave_west_cost = field.field[wave_west.as_1d_index()];
							// see if diagonally blocking
							if wave_west_cost & INT_BITS_IMPASSABLE != INT_BITS_IMPASSABLE {
								// not blocking, see if neighbour can be made a corner
								if let Some(n_west) =
									neighbour.get_in_compass_direction(&CompassDir::West, 1)
								{
									let n_west_cost = field.field[n_west.as_1d_index()];
									if n_west_cost & INT_BITS_IMPASSABLE != INT_BITS_IMPASSABLE {
										extend_los_corner(
											field,
											&n_west,
											&goal_cell,
											wavefront_cost,
										);
									}
								}
							}
						}
						if let Some(wave_east) =
							wave_cell.get_in_compass_direction(&CompassDir::East, 1)
						{
							let wave_east_cost = field.field[wave_east.as_1d_index()];
							// see if diagonally blocking
							if wave_east_cost & INT_BITS_IMPASSABLE != INT_BITS_IMPASSABLE {
								// not blocking, see if neighbour can be made a corner
								if let Some(n_east) =
									neighbour.get_in_compass_direction(&CompassDir::East, 1)
								{
									let n_east_cost = field.field[n_east.as_1d_index()];
									if n_east_cost & INT_BITS_IMPASSABLE != INT_BITS_IMPASSABLE {
										extend_los_corner(
											field,
											&n_east,
											&goal_cell,
											wavefront_cost,
										);
									}
								}
							}
						}
					}
					CompassDir::East | CompassDir::West => {
						if let Some(wave_north) =
							wave_cell.get_in_compass_direction(&CompassDir::North, 1)
						{
							let wave_north_cost = field.field[wave_north.as_1d_index()];
							// see if diagonally blocking
							if wave_north_cost & INT_BITS_IMPASSABLE != INT_BITS_IMPASSABLE {
								// not blocking, see if neighbour can be made a corner
								if let Some(n_north) =
									neighbour.get_in_compass_direction(&CompassDir::North, 1)
								{
									let n_north_cost = field.field[n_north.as_1d_index()];
									if n_north_cost & INT_BITS_IMPASSABLE != INT_BITS_IMPASSABLE {
										extend_los_corner(
											field,
											&n_north,
											&goal_cell,
											wavefront_cost,
										);
									}
								}
							}
						}
						if let Some(wave_south) =
							wave_cell.get_in_compass_direction(&CompassDir::South, 1)
						{
							let wave_south_cost = field.field[wave_south.as_1d_index()];
							// see if diagonally blocking
							if wave_south_cost & INT_BITS_IMPASSABLE != INT_BITS_IMPASSABLE {
								// not blocking, see if neighbour can be made a corner
								if let Some(n_south) =
									neighbour.get_in_compass_direction(&CompassDir::South, 1)
								{
									let n_south_cost = field.field[n_south.as_1d_index()];
									if n_south_cost & INT_BITS_IMPASSABLE != INT_BITS_IMPASSABLE {
										extend_los_corner(
											field,
											&n_south,
											&goal_cell,
											wavefront_cost,
										);
									}
								}
							}
						}
					}
					_ => panic!("Only orthogonal CompassDir should be here, found {}", dir),
				}
			} else if cost & INT_BITS_LOS != INT_BITS_LOS {
				// we have a new LOS that can be propagated,
				// set the integration value as the wavefront
				// and set the LOS flag
				let mut value = wavefront_cost;
				value |= INT_BITS_LOS;
				field.field[n_index] = value;
				next_wavefront.push(n_index);
			}
		}
	}
	wavefront_cost += 1;
	// if valid cells exist to continue propagation then recursively propagate LOS
	if !next_wavefront.is_empty() {
		propagate_los(field, &next_wavefront, wavefront_cost, goal);
	}
}

/// Establish a line from the `goal` that runs through the `corner` and hits
/// the sector boundary. Any cells after `corner` to the boundary should be
/// marked as corners and flagged to prevent LOS propagation from flowing
/// around the corner and out of sight
fn extend_los_corner(
	field: &mut IntegrationField,
	corner: &FieldCell,
	goal: &FieldCell,
	wavefront_cost: u32,
) {
	// find the sector edge where line of sight should be blocked based on the corner
	let end = check_los_corner_propagation(corner, goal);
	// from the corner to the boundary cell of LOS being blocked use the bresenham line algorithm to find all cells between the two cell points and mark them as being wavefront blocked so that further LOS propagation won't flow behind impassable cells
	let blocked_cells = corner.get_cells_between_points(&end);
	for (i, blocked) in blocked_cells.iter().enumerate() {
		let value = field.get_field_cell_value(*blocked);
		// only mark flags for cells that aren't walls
		if value & INT_BITS_IMPASSABLE == INT_BITS_IMPASSABLE {
			break;
		}
		// if the line passes through the diagonal of two impassable cells propagation should stop otherwise a line of corners would be assigned that's not reachable from the corner being extrapolated
		if i > 0 {
			let previous = &blocked_cells[i - 1];
			match CompassDir::cell_to_cell_direction(*blocked, *previous) {
				CompassDir::NorthEast => {
					if let Some(south) = CompassDir::get_cell_neighbour(*blocked, CompassDir::South)
						&& let Some(west) =
							CompassDir::get_cell_neighbour(*blocked, CompassDir::West)
					{
						let s_v = field.get_field_cell_value(south) & INT_BITS_IMPASSABLE;
						let w_v = field.get_field_cell_value(west) & INT_BITS_IMPASSABLE;
						if s_v == INT_BITS_IMPASSABLE && w_v == INT_BITS_IMPASSABLE {
							break;
						}
					}
				}
				CompassDir::SouthEast => {
					if let Some(north) = CompassDir::get_cell_neighbour(*blocked, CompassDir::North)
						&& let Some(west) =
							CompassDir::get_cell_neighbour(*blocked, CompassDir::West)
					{
						let n_v = field.get_field_cell_value(north) & INT_BITS_IMPASSABLE;
						let w_v = field.get_field_cell_value(west) & INT_BITS_IMPASSABLE;
						if n_v == INT_BITS_IMPASSABLE && w_v == INT_BITS_IMPASSABLE {
							break;
						}
					}
				}
				CompassDir::SouthWest => {
					if let Some(north) = CompassDir::get_cell_neighbour(*blocked, CompassDir::North)
						&& let Some(east) =
							CompassDir::get_cell_neighbour(*blocked, CompassDir::East)
					{
						let n_v = field.get_field_cell_value(north) & INT_BITS_IMPASSABLE;
						let e_v = field.get_field_cell_value(east) & INT_BITS_IMPASSABLE;
						if n_v == INT_BITS_IMPASSABLE && e_v == INT_BITS_IMPASSABLE {
							break;
						}
					}
				}
				CompassDir::NorthWest => {
					if let Some(south) = CompassDir::get_cell_neighbour(*blocked, CompassDir::South)
						&& let Some(east) =
							CompassDir::get_cell_neighbour(*blocked, CompassDir::East)
					{
						let s_v = field.get_field_cell_value(south) & INT_BITS_IMPASSABLE;
						let e_v = field.get_field_cell_value(east) & INT_BITS_IMPASSABLE;
						if s_v == INT_BITS_IMPASSABLE && e_v == INT_BITS_IMPASSABLE {
							break;
						}
					}
				}
				CompassDir::Zero => panic!("Neighbour not found"),
				_ => {}
			}
		}
		// don't mark cells which aren't already marked as wavefront blocked
		if value & INT_BITS_WAVE_BLOCKED != INT_BITS_WAVE_BLOCKED {
			// mark the line as corners for the int calc layer
			field.los_corners.push(blocked.as_1d_index());
			// NB: add 1 because corner is effectively one wavefront propagation ahead
			// then add `i` as each successive line cells is another wavefront ahead
			field.set_field_cell_value(
				wavefront_cost + 1 + i as u32 + INT_BITS_WAVE_BLOCKED + INT_BITS_CORNER,
				*blocked,
			);
		}
	}
}

/// Construct a vector from the `goal` to the `corner` [FieldCell] and extrapolate it so that it intersects a sector boundary. Based on the `FieldCells` crossed by the line wavefront propagation can be blocked to ensure that the LOS propagation doesn't flow around obscured corners. This method will produce the boundary [FieldCell] that can be plugged into the Bresenham Line Algorithm to determine the blocked cells
fn check_los_corner_propagation(corner: &FieldCell, goal: &FieldCell) -> FieldCell {
	// obtain wavefront blocked from the corner,
	// using the line equation properties we find the vector
	// from the goal to the corner and then find from
	// the corner what FieldCell on the Sector boundary the
	// line would terminate at
	//
	// deal with vertical and horizontal lines first
	if corner.get_column() == goal.get_column() {
		// no column change, find direction
		// of row change
		if corner.get_row() > goal.get_row() {
			// dir is heading down to max boundary value
			FieldCell::new(corner.get_column(), FIELD_RESOLUTION - 1)
		} else {
			// dir is heading up towards boundary 0
			FieldCell::new(corner.get_column(), 0)
		}
	} else if corner.get_row() == goal.get_row() {
		// no row change, find direction of
		// column change
		if corner.get_column() > goal.get_column() {
			// dir is heading right towards max boundary
			FieldCell::new(FIELD_RESOLUTION - 1, corner.get_row())
		} else {
			// dir is heading left towards boundary 0
			FieldCell::new(0, corner.get_row())
		}
	} else {
		// handle diagonal lines
		let delta_column = corner.get_column() as f32 - goal.get_column() as f32;
		let delta_row = corner.get_row() as f32 - goal.get_row() as f32;
		let gradient = delta_row / delta_column;
		let intercept = -gradient * (corner.get_column() as f32) + corner.get_row() as f32;
		if corner.get_column() > goal.get_column() {
			// walk the line with increasing column
			// until the row or column value
			// reaches a sector boundary
			let d = (FIELD_RESOLUTION - 1)
				.checked_sub(corner.get_column())
				.unwrap();
			for x in 0..=d {
				let end_col = corner.get_column() + x;
				let end_row = (gradient * (end_col as f32) + intercept).floor();
				// handle steep lines, e.g goal (4,4) and adj (5,7) projected
				// along column places column 6 on row 10 which is OOB
				if end_row > FIELD_RESOLUTION as f32 - 1.0 {
					if end_col < FIELD_RESOLUTION {
						return FieldCell::new(end_col, FIELD_RESOLUTION - 1);
					} else {
						return FieldCell::new(FIELD_RESOLUTION - 1, FIELD_RESOLUTION - 1);
					}
				} else if end_row < 0.0 {
					if end_col < FIELD_RESOLUTION {
						return FieldCell::new(end_col, 0);
					} else {
						return FieldCell::new(FIELD_RESOLUTION - 1, 0);
					}
				} else if end_col == FIELD_RESOLUTION - 1 {
					return FieldCell::new(end_col, end_row as usize);
				}
			}
			//TODO make this better
			panic!("LOS corner prop failed to find increment boundary");
		} else {
			// walk the line with decreasing column
			// until row or column value
			// reaches a sector boundary
			let d = corner.get_column();
			for x in 0..=d {
				let end_col = corner.get_column().checked_sub(x).unwrap();
				let end_row = (gradient * (end_col as f32) + intercept).floor() as usize;
				// handle steep cases where line projection is OOB
				// ex: goal (7,5), adj (6,9), projects (0,33)
				if end_col == 0 {
					if end_row > FIELD_RESOLUTION - 1 {
						return FieldCell::new(end_col, FIELD_RESOLUTION - 1);
					} else {
						return FieldCell::new(end_col, end_row);
					}
				}
				if end_row == 0 {
					return FieldCell::new(end_col, end_row);
				}
				if end_row > FIELD_RESOLUTION - 1 {
					return FieldCell::new(end_col, FIELD_RESOLUTION - 1);
				}
			}
			//TODO make this better
			panic!("LOS corner prop failed to find decrement boundary");
		}
	}
}

//TODO this is a diamond shaped propagation, spherical propagation would be more accurate? (spherical is solving the Eikonal PDE). Also this wastes lookups inspecting previously calculated cells, should visit only once
/// Expand the wavefront recursively and produce the integrated-cost of cells
/// in the [IntegrationField]
fn propagate_integrated_wavefront(
	int_field: &mut IntegrationField,
	costfield: &CostField,
	wavefront: Vec<(usize, u32)>,
) {
	let mut next_wavefront = vec![];
	for (cell_index, prev_int_cost) in wavefront.iter() {
		let neighbours = FieldCell::from_index(*cell_index).get_orthogonal_neighbours();
		for n in neighbours.iter() {
			// ensure neighbour isn't impassable or LOS
			let n_int = int_field.get_field_cell_value(*n);
			if n_int & INT_BITS_IMPASSABLE != INT_BITS_IMPASSABLE
				&& n_int & INT_BITS_LOS != INT_BITS_LOS
			{
				let cell_cost = costfield.get_field_cell_value(*n) as u32;
				let int_cost = cell_cost + (prev_int_cost & INT_FILTER_BITS_COST);
				// if this neighbour has been calculated with a cheaper value then
				// update it
				//TODO does this overwrite any required flags
				if int_cost < (n_int & INT_FILTER_BITS_COST) {
					int_field.set_field_cell_value(int_cost, *n);
					next_wavefront.push((n.as_1d_index(), int_cost));
				}
			}
		}
	}

	if !next_wavefront.is_empty() {
		propagate_integrated_wavefront(int_field, costfield, next_wavefront);
	}
}

// #[rustfmt::skip]
#[cfg(test)]
mod tests {
	use super::*;
	use crate::flowfields::{portal::PortalWindow, sectors::SectorID};

	#[test]
	fn goal_set_final() {
		let costfield = CostField::default();
		let sector = SectorID::new(1, 1);
		let goal = 14;
		let portal = None;
		let route_step = RouteStep::new(&sector, goal, portal);
		let int_field = IntegrationField::init(&costfield, &route_step);

		assert!(int_field.field[14] & INT_BITS_GOAL == INT_BITS_GOAL);
	}

	#[test]
	fn goal_set_portal() {
		let costfield = CostField::default();
		let sector = SectorID::new(1, 1);
		let goal = 94;
		let portal = Some(PortalWindow::new(
			FieldCell::new(0, 9),
			FieldCell::new(9, 9),
			CompassDir::South,
		));
		let route_step = RouteStep::new(&sector, goal, portal);
		let int_field = IntegrationField::init(&costfield, &route_step);

		assert!(int_field.field[90] & INT_BITS_PORTAL == INT_BITS_PORTAL);
		assert!(int_field.field[91] & INT_BITS_PORTAL == INT_BITS_PORTAL);
		assert!(int_field.field[92] & INT_BITS_PORTAL == INT_BITS_PORTAL);
		assert!(int_field.field[93] & INT_BITS_PORTAL == INT_BITS_PORTAL);
		assert!(int_field.field[94] & INT_BITS_PORTAL == INT_BITS_PORTAL);
		assert!(int_field.field[95] & INT_BITS_PORTAL == INT_BITS_PORTAL);
		assert!(int_field.field[96] & INT_BITS_PORTAL == INT_BITS_PORTAL);
		assert!(int_field.field[97] & INT_BITS_PORTAL == INT_BITS_PORTAL);
		assert!(int_field.field[98] & INT_BITS_PORTAL == INT_BITS_PORTAL);
		assert!(int_field.field[99] & INT_BITS_PORTAL == INT_BITS_PORTAL);
	}

	// g = goal
	// X = wall
	// L = LOS
	// b = wave blocked
	// ```txt
	//  ___ ___ ___ ___ ___ ___ ___ ___ ___ ___
	// |   |   |   |   |   |   |   |   |   |   |
	// |___|___|___|___|___|___|___|___|___|___|
	// |   |   |   |   |   |   |   |   |   |   |
	// |___|___|___|___|___|___|___|___|___|___|
	// |   |   |   |   |   |   |   |   |   | b |
	// |___|___|___|___|___|___|___|___|___|___|
	// | b |   |   |   |   |   |   |   | b | L |
	// |___|___|___|___|___|___|___|___|___|___|
	// | L | b |   |   |   |   |   | b | L | L |
	// |___|___|___|___|___|___|___|___|___|___|
	// | L | L | b | X | X | X | b | L | L | L |
	// |___|___|___|___|___|___|___|___|___|___|
	// | L | L | L | L | L | L | L | L | L | L |
	// |___|___|___|___|___|___|___|___|___|___|
	// | L | L | L | L | L | L | L | L | L | L |
	// |___|___|___|___|___|___|___|___|___|___|
	// | L | L | L | L | g | L | L | L | L | L |
	// |___|___|___|___|___|___|___|___|___|___|
	// | L | b | X | L | L | L | X | b | L | L |
	// |___|___|___|___|___|___|___|___|___|___|
	// ```
	#[test]
	fn check_los() {
		let mut costfield = CostField::default();
		costfield.set_field_cell_value(255, FieldCell::new(3, 5));
		costfield.set_field_cell_value(255, FieldCell::new(4, 5));
		costfield.set_field_cell_value(255, FieldCell::new(5, 5));
		costfield.set_field_cell_value(255, FieldCell::new(2, 9));
		costfield.set_field_cell_value(255, FieldCell::new(6, 9));
		let sector = SectorID::new(1, 1);
		let goal = 84;
		let portal = None;
		let route_step = RouteStep::new(&sector, goal, portal);
		let int_field = IntegrationField::init(&costfield, &route_step);

		let r_c = FieldCell::new(1, 9);
		let r = int_field.get_field_cell_value(r_c);
		println!(
			"{} :: flags: {:#034b}, cost: {:#034b}",
			r_c,
			r & INT_FILTER_BITS_FLAGS,
			r & INT_FILTER_BITS_COST
		);
		assert!(r & INT_BITS_WAVE_BLOCKED == INT_BITS_WAVE_BLOCKED);

		let r_c = FieldCell::new(2, 5);
		let r = int_field.get_field_cell_value(r_c);
		println!(
			"{} :: flags: {:#034b}, cost: {:#034b}",
			r_c,
			r & INT_FILTER_BITS_FLAGS,
			r & INT_FILTER_BITS_COST
		);
		assert!(r & INT_BITS_WAVE_BLOCKED == INT_BITS_WAVE_BLOCKED);

		let r_c = FieldCell::new(1, 4);
		let r = int_field.get_field_cell_value(r_c);
		println!(
			"{} :: flags: {:#034b}, cost: {:#034b}",
			r_c,
			r & INT_FILTER_BITS_FLAGS,
			r & INT_FILTER_BITS_COST
		);
		assert!(r & INT_BITS_WAVE_BLOCKED == INT_BITS_WAVE_BLOCKED);

		let r_c = FieldCell::new(3, 8);
		let r = int_field.get_field_cell_value(r_c);
		println!(
			"{} :: flags: {:#034b}, cost: {:#034b}",
			r_c,
			r & INT_FILTER_BITS_FLAGS,
			r & INT_FILTER_BITS_COST
		);
		assert!(r & INT_BITS_LOS == INT_BITS_LOS);

		//TODO: interesting problem. As the LOS propagation works round in a clockwise
		//TODO: fashion (2, 8) is marked as blocked even tho it is parallel to the
		//TODO: goal. (2, 9) is analysed before (2, 8) meaning the north of the
		//TODO: wall is treated as a corner
		let r_c = FieldCell::new(2, 8);
		let r = int_field.get_field_cell_value(r_c);
		println!(
			"{} :: flags: {:#034b}, cost: {:#034b}",
			r_c,
			r & INT_FILTER_BITS_FLAGS,
			r & INT_FILTER_BITS_COST
		);
		//TODO Should really be LOS here
		// assert!(r & INT_BITS_LOS == INT_BITS_LOS);
		assert!(r & INT_BITS_WAVE_BLOCKED == INT_BITS_WAVE_BLOCKED);
	}
}