TLDR: Walk the geometry of the grid by following the slope of the line. Provide the computer the instructions you'd follow tracing the line yourself with a ruler.
Below I will share an unoptimized C# implementation. The core challenge here is determining which grid cells the traced line will pass through between two corners. Some additional complication is added by the 4e rule about handling line of effect along edges, and so edge pairs are also returned. Those can be tested to see if the cells on both sides of the line are blocked to prevent line of effect from going perpendicularly through a wall.
The function calls itself with swapped parameters to reduce the number of cases. I assume GridCellsBetweenCorners is O(n) where n = the line's length, but maybe someone still in university can confirm.
public static (List<Vector2I> coveredCells, List<Tuple<Vector2I, Vector2I>> edgePairs) GridCellsBetweenCorners(Vector2I origin, Vector2I target) {
//origin and destination points are specified by indicating the coordinates of the grid cell that has that point as the top-left corner
List<Vector2I> coveredCells = [];
List<Tuple<Vector2I, Vector2I>> edgePairs = [];
Vector2I diff = target - origin;
Vector2I diffSign = diff.Sign();
if(diffSign.X < 0) {
return GridCellsBetweenCorners(target, origin);
}
if(origin == target) {
//no covered cells
} else if(diff.X == 0 || diff.Y == 0) {
//parallel to the x or y axis
//edges don't count as intersections, but they need to be checked to see if both sides of an edge are blocked
if(diffSign.Y < 0) {
return GridCellsBetweenCorners(target, origin);
}
Vector2I walk = diff.Sign();
Vector2I edgeCheckOffset = diff.X == 0 ? Vector2I.Left : Vector2I.Up;
int distance = Math.Max(diff.X, diff.Y);
for(int i = 0; i < distance; i++) {
Vector2I posEdge = origin + (walk * i);
edgePairs.Add(Tuple.Create(posEdge, posEdge + edgeCheckOffset));
}
} else if(Math.Abs(diff.X) == Math.Abs(diff.Y)) {
//slope of 1 or -1
Vector2I walk = diffSign;
Vector2I offset = diff.Y < 0 ? Vector2I.Up : Vector2I.Zero;
offset += diff.X < 0 ? Vector2I.Left : Vector2I.Zero;
int distance = Math.Abs(diff.X);
for(int i = 0; i < distance; i++) {
coveredCells.Add(origin + offset + (walk * i));
}
} else {
int prime = Math.Abs(diff.X) > Math.Abs(diff.Y) ? 0 : 1; //axis index
int aux = prime == 1 ? 0 : 1; //axis index
int distance = Math.Abs(target[prime] - origin[prime]);
bool isSlopePositive = diffSign.X == diffSign.Y;
Vector2I walk = Vector2I.Zero;
Vector2I offset = isSlopePositive ? Vector2I.Zero : Vector2I.Up;
for(int i = 0; i < distance; i++) {
coveredCells.Add(origin + walk + offset);
if(Math.Abs((i + 1) * diff[aux] / diff[prime]) != Math.Abs(walk[aux])) {
walk[aux] += (isSlopePositive || prime == 1) ? 1 : -1;
if(i + 1 != distance && diff[prime] % diff[aux] != 0) {
coveredCells.Add(origin + walk + offset);
}
}
walk[prime] += (isSlopePositive || prime == 0) ? 1 : -1;
}
}
return (coveredCells, edgePairs);
}
Assuming you want to implement the 4e line of effect rules, you'll have to call it 16 times and remove the cells the units are standing in...
public static (List<List<Vector2I>> cells, List<List<Tuple<Vector2I, Vector2I>>> edgePairs) TraceSightLines(Vector2I originCell, Vector2I targetCell) {
List<List<Vector2I>> result = [];
List<List<Tuple<Vector2I, Vector2I>>> result2 = [];
Vector2I[] offsets = [Vector2I.Zero, Vector2I.Right, Vector2I.Down, Vector2I.One];
for(int i = 0; i < offsets.Length; i++) {
for(int j = 0; j < offsets.Length; j++) {
(List<Vector2I> cells, List<Tuple<Vector2I, Vector2I>> edgePairs) = GridCellsBetweenCorners(originCell + offsets[i], targetCell + offsets[j]);
cells.Remove(originCell);
cells.Remove(targetCell);
result.Add(cells);
List<Tuple<Vector2I, Vector2I>> edgesPairsToKeep = [];
foreach(Tuple<Vector2I, Vector2I> pair in edgePairs) {
if(pair.Item1 != originCell && pair.Item2 != originCell
&& pair.Item1 != targetCell && pair.Item2 != targetCell) {
edgesPairsToKeep.Add(pair);
}
}
result2.Add(edgesPairsToKeep);
}
}
return (result, result2);
}
...then test against the grid data and select the most favorable vertex to trace from with regards to cover.
var (lines, edgePairs) = TraceSightLines(_activeUnit.Cell, newCell);
bool _isLineBlocked(List<Vector2I> line) {
foreach(Vector2I cell in line) {
if(IsBlocked(cell)) {
return true;
}
}
return false;
}
bool _isEdgeBlocked(List<Tuple<Vector2I, Vector2I>> pairs) {
foreach(var pair in pairs) {
if(IsBlocked(pair.Item1) && IsBlocked(pair.Item2)) {
return true;
}
}
return false;
}
Vector2 traceOrigin = CalcCornerMapPositions(_activeUnit.Cell)[0];
int countMin = int.MaxValue;
Color[] coverColors = new Color[4];
for(int i = 0; i < lines.Count;) {
bool tL = _isLineBlocked(lines[i]) || _isEdgeBlocked(edgePairs[i]); i++;
bool tR = _isLineBlocked(lines[i]) || _isEdgeBlocked(edgePairs[i]); i++;
bool bL = _isLineBlocked(lines[i]) || _isEdgeBlocked(edgePairs[i]); i++;
bool bR = _isLineBlocked(lines[i]) || _isEdgeBlocked(edgePairs[i]); i++;
int count = (tL ? 1 : 0) + (tR ? 1 : 0) + (bL ? 1 : 0) + (bR ? 1 : 0);
if(count < countMin) {
countMin = count;
traceOrigin = CalcCornerMapPositions(_activeUnit.Cell)[(i - 4) / 4];
coverColors[0] = tL ? Colors.Red : Colors.Green;
coverColors[1] = tR ? Colors.Red : Colors.Green;
coverColors[2] = bL ? Colors.Red : Colors.Green;
coverColors[3] = bR ? Colors.Red : Colors.Green;
}
}
_unitPath.PaintPath([], traceOrigin, CalcCornerMapPositions(newCell), coverColors);