|
|
@@ -0,0 +1,324 @@ |
|
|
|
""" |
|
|
|
Author: Aaron MacDonald |
|
|
|
Date: June 14, 2007 |
|
|
|
|
|
|
|
Description: An implementation of the precise permissive field |
|
|
|
of view algorithm for use in tile-based games. |
|
|
|
Based on the algorithm presented at |
|
|
|
http://roguebasin.roguelikedevelopment.org/ |
|
|
|
index.php?title= |
|
|
|
Precise_Permissive_Field_of_View. |
|
|
|
|
|
|
|
You are free to use or modify this code as long as this notice is |
|
|
|
included. |
|
|
|
This code is released without warranty. |
|
|
|
""" |
|
|
|
|
|
|
|
import copy |
|
|
|
|
|
|
|
def fieldOfView(startX, startY, mapWidth, mapHeight, radius, \ |
|
|
|
funcVisitTile, funcTileBlocked): |
|
|
|
""" |
|
|
|
Determines which coordinates on a 2D grid are visible from a |
|
|
|
particular coordinate. |
|
|
|
|
|
|
|
startX, startY: The (x, y) coordinate on the grid that |
|
|
|
is the centre of view. |
|
|
|
|
|
|
|
mapWidth, mapHeight: The maximum extents of the grid. The |
|
|
|
minimum extents are assumed to be both |
|
|
|
zero. |
|
|
|
|
|
|
|
radius: How far the field of view may extend |
|
|
|
in either direction along the x and y |
|
|
|
axis. |
|
|
|
|
|
|
|
funcVisitTile: User function that takes two integers |
|
|
|
representing an (x, y) coordinate. Is |
|
|
|
used to "visit" visible coordinates. |
|
|
|
|
|
|
|
funcTileBlocked: User function that takes two integers |
|
|
|
representing an (x, y) coordinate. |
|
|
|
Returns True if the coordinate blocks |
|
|
|
sight to coordinates "behind" it. |
|
|
|
""" |
|
|
|
|
|
|
|
visited = set() # Keep track of what tiles have been visited so |
|
|
|
# that no tile will be visited twice. |
|
|
|
|
|
|
|
# Will always see the centre. |
|
|
|
funcVisitTile(startX, startY) |
|
|
|
visited.add((startX, startY)) |
|
|
|
|
|
|
|
# Ge the dimensions of the actual field of view, making |
|
|
|
# sure not to go off the map or beyond the radius. |
|
|
|
|
|
|
|
if startX < radius: |
|
|
|
minExtentX = startX |
|
|
|
else: |
|
|
|
minExtentX = radius |
|
|
|
|
|
|
|
if mapWidth - startX - 1 < radius: |
|
|
|
maxExtentX = mapWidth - startX - 1 |
|
|
|
else: |
|
|
|
maxExtentX = radius |
|
|
|
|
|
|
|
if startY < radius: |
|
|
|
minExtentY = startY |
|
|
|
else: |
|
|
|
minExtentY = radius |
|
|
|
|
|
|
|
if mapHeight - startY - 1 < radius: |
|
|
|
maxExtentY = mapHeight - startY - 1 |
|
|
|
else: |
|
|
|
maxExtentY = radius |
|
|
|
|
|
|
|
# Northeast quadrant |
|
|
|
__checkQuadrant(visited, startX, startY, 1, 1, \ |
|
|
|
maxExtentX, maxExtentY, \ |
|
|
|
funcVisitTile, funcTileBlocked) |
|
|
|
|
|
|
|
# Southeast quadrant |
|
|
|
__checkQuadrant(visited, startX, startY, 1, -1, \ |
|
|
|
maxExtentX, minExtentY, \ |
|
|
|
funcVisitTile, funcTileBlocked) |
|
|
|
|
|
|
|
# Southwest quadrant |
|
|
|
__checkQuadrant(visited, startX, startY, -1, -1, \ |
|
|
|
minExtentX, minExtentY, \ |
|
|
|
funcVisitTile, funcTileBlocked) |
|
|
|
|
|
|
|
# Northwest quadrant |
|
|
|
__checkQuadrant(visited, startX, startY, -1, 1, \ |
|
|
|
minExtentX, maxExtentY, \ |
|
|
|
funcVisitTile, funcTileBlocked) |
|
|
|
|
|
|
|
#------------------------------------------------------------- |
|
|
|
|
|
|
|
class __Line(object): |
|
|
|
def __init__(self, xi, yi, xf, yf): |
|
|
|
self.xi = xi |
|
|
|
self.yi = yi |
|
|
|
self.xf = xf |
|
|
|
self.yf = yf |
|
|
|
|
|
|
|
dx = property(fget = lambda self: self.xf - self.xi) |
|
|
|
dy = property(fget = lambda self: self.yf - self.yi) |
|
|
|
|
|
|
|
def pBelow(self, x, y): |
|
|
|
return self.relativeSlope(x, y) > 0 |
|
|
|
|
|
|
|
def pBelowOrCollinear(self, x, y): |
|
|
|
return self.relativeSlope(x, y) >= 0 |
|
|
|
|
|
|
|
def pAbove(self, x, y): |
|
|
|
return self.relativeSlope(x, y) < 0 |
|
|
|
|
|
|
|
def pAboveOrCollinear(self, x, y): |
|
|
|
return self.relativeSlope(x, y) <= 0 |
|
|
|
|
|
|
|
def pCollinear(self, x, y): |
|
|
|
return self.relativeSlope(x, y) == 0 |
|
|
|
|
|
|
|
def lineCollinear(self, line): |
|
|
|
return self.pCollinear(line.xi, line.yi) \ |
|
|
|
and self.pCollinear(line.xf, line.yf) |
|
|
|
|
|
|
|
def relativeSlope(self, x, y): |
|
|
|
return (self.dy * (self.xf - x)) \ |
|
|
|
- (self.dx * (self.yf - y)) |
|
|
|
|
|
|
|
class __ViewBump: |
|
|
|
def __init__(self, x, y, parent): |
|
|
|
self.x = x |
|
|
|
self.y = y |
|
|
|
self.parent = parent |
|
|
|
|
|
|
|
class __View: |
|
|
|
def __init__(self, shallowLine, steepLine): |
|
|
|
self.shallowLine = shallowLine |
|
|
|
self.steepLine = steepLine |
|
|
|
|
|
|
|
self.shallowBump = None |
|
|
|
self.steepBump = None |
|
|
|
|
|
|
|
def __checkQuadrant(visited, startX, startY, dx, dy, \ |
|
|
|
extentX, extentY, funcVisitTile, funcTileBlocked): |
|
|
|
activeViews = [] |
|
|
|
|
|
|
|
shallowLine = __Line(0, 1, extentX, 0) |
|
|
|
steepLine = __Line(1, 0, 0, extentY) |
|
|
|
|
|
|
|
activeViews.append( __View(shallowLine, steepLine) ) |
|
|
|
viewIndex = 0 |
|
|
|
|
|
|
|
# Visit the tiles diagonally and going outwards |
|
|
|
# |
|
|
|
# . |
|
|
|
# . |
|
|
|
# . . |
|
|
|
# 9 . |
|
|
|
# 5 8 . |
|
|
|
# 2 4 7 |
|
|
|
# @ 1 3 6 . . . |
|
|
|
maxI = extentX + extentY |
|
|
|
i = 1 |
|
|
|
while i != maxI + 1 and len(activeViews) > 0: |
|
|
|
if 0 > i - extentX: |
|
|
|
startJ = 0 |
|
|
|
else: |
|
|
|
startJ = i - extentX |
|
|
|
|
|
|
|
if i < extentY: |
|
|
|
maxJ = i |
|
|
|
else: |
|
|
|
maxJ = extentY |
|
|
|
|
|
|
|
j = startJ |
|
|
|
while j != maxJ + 1 and viewIndex < len(activeViews): |
|
|
|
x = i - j |
|
|
|
y = j |
|
|
|
__visitCoord(visited, startX, startY, x, y, dx, dy, \ |
|
|
|
viewIndex, activeViews, \ |
|
|
|
funcVisitTile, funcTileBlocked) |
|
|
|
|
|
|
|
j += 1 |
|
|
|
|
|
|
|
i += 1 |
|
|
|
|
|
|
|
def __visitCoord(visited, startX, startY, x, y, dx, dy, viewIndex, \ |
|
|
|
activeViews, funcVisitTile, funcTileBlocked): |
|
|
|
# The top left and bottom right corners of the current coordinate. |
|
|
|
topLeft = (x, y + 1) |
|
|
|
bottomRight = (x + 1, y) |
|
|
|
|
|
|
|
while viewIndex < len(activeViews) \ |
|
|
|
and activeViews[viewIndex].steepLine.pBelowOrCollinear( \ |
|
|
|
bottomRight[0], bottomRight[1]): |
|
|
|
# The current coordinate is above the current view and is |
|
|
|
# ignored. The steeper fields may need it though. |
|
|
|
viewIndex += 1 |
|
|
|
|
|
|
|
if viewIndex == len(activeViews) \ |
|
|
|
or activeViews[viewIndex].shallowLine.pAboveOrCollinear( \ |
|
|
|
topLeft[0], topLeft[1]): |
|
|
|
# Either the current coordinate is above all of the fields |
|
|
|
# or it is below all of the fields. |
|
|
|
return |
|
|
|
|
|
|
|
# It is now known that the current coordinate is between the steep |
|
|
|
# and shallow lines of the current view. |
|
|
|
|
|
|
|
isBlocked = False |
|
|
|
|
|
|
|
# The real quadrant coordinates |
|
|
|
realX = x * dx |
|
|
|
realY = y * dy |
|
|
|
|
|
|
|
if (startX + realX, startY + realY) not in visited: |
|
|
|
visited.add((startX + realX, startY + realY)) |
|
|
|
funcVisitTile(startX + realX, startY + realY) |
|
|
|
"""else: |
|
|
|
# Debugging |
|
|
|
print (startX + realX, startY + realY)""" |
|
|
|
|
|
|
|
isBlocked = funcTileBlocked(startX + realX, startY + realY) |
|
|
|
|
|
|
|
if not isBlocked: |
|
|
|
# The current coordinate does not block sight and therefore |
|
|
|
# has no effect on the view. |
|
|
|
return |
|
|
|
|
|
|
|
if activeViews[viewIndex].shallowLine.pAbove( \ |
|
|
|
bottomRight[0], bottomRight[1]) \ |
|
|
|
and activeViews[viewIndex].steepLine.pBelow( \ |
|
|
|
topLeft[0], topLeft[1]): |
|
|
|
# The current coordinate is intersected by both lines in the |
|
|
|
# current view. The view is completely blocked. |
|
|
|
del activeViews[viewIndex] |
|
|
|
elif activeViews[viewIndex].shallowLine.pAbove( \ |
|
|
|
bottomRight[0], bottomRight[1]): |
|
|
|
# The current coordinate is intersected by the shallow line of |
|
|
|
# the current view. The shallow line needs to be raised. |
|
|
|
__addShallowBump(topLeft[0], topLeft[1], \ |
|
|
|
activeViews, viewIndex) |
|
|
|
__checkView(activeViews, viewIndex) |
|
|
|
elif activeViews[viewIndex].steepLine.pBelow( \ |
|
|
|
topLeft[0], topLeft[1]): |
|
|
|
# The current coordinate is intersected by the steep line of |
|
|
|
# the current view. The steep line needs to be lowered. |
|
|
|
__addSteepBump(bottomRight[0], bottomRight[1], activeViews, \ |
|
|
|
viewIndex) |
|
|
|
__checkView(activeViews, viewIndex) |
|
|
|
else: |
|
|
|
# The current coordinate is completely between the two lines |
|
|
|
# of the current view. Split the current view into two views |
|
|
|
# above and below the current coordinate. |
|
|
|
|
|
|
|
shallowViewIndex = viewIndex |
|
|
|
viewIndex += 1 |
|
|
|
steepViewIndex = viewIndex |
|
|
|
|
|
|
|
activeViews.insert(shallowViewIndex, \ |
|
|
|
copy.deepcopy(activeViews[shallowViewIndex])) |
|
|
|
|
|
|
|
__addSteepBump(bottomRight[0], bottomRight[1], \ |
|
|
|
activeViews, shallowViewIndex) |
|
|
|
if not __checkView(activeViews, shallowViewIndex): |
|
|
|
viewIndex -= 1 |
|
|
|
steepViewIndex -= 1 |
|
|
|
|
|
|
|
__addShallowBump(topLeft[0], topLeft[1], activeViews, \ |
|
|
|
steepViewIndex) |
|
|
|
__checkView(activeViews, steepViewIndex) |
|
|
|
|
|
|
|
def __addShallowBump(x, y, activeViews, viewIndex): |
|
|
|
activeViews[viewIndex].shallowLine.xf = x |
|
|
|
activeViews[viewIndex].shallowLine.yf = y |
|
|
|
|
|
|
|
activeViews[viewIndex].shallowBump = __ViewBump(x, y, \ |
|
|
|
activeViews[viewIndex].shallowBump) |
|
|
|
|
|
|
|
curBump = activeViews[viewIndex].steepBump |
|
|
|
while curBump is not None: |
|
|
|
if activeViews[viewIndex].shallowLine.pAbove( \ |
|
|
|
curBump.x, curBump.y): |
|
|
|
activeViews[viewIndex].shallowLine.xi = curBump.x |
|
|
|
activeViews[viewIndex].shallowLine.yi = curBump.y |
|
|
|
|
|
|
|
curBump = curBump.parent |
|
|
|
|
|
|
|
def __addSteepBump(x, y, activeViews, viewIndex): |
|
|
|
activeViews[viewIndex].steepLine.xf = x |
|
|
|
activeViews[viewIndex].steepLine.yf = y |
|
|
|
|
|
|
|
activeViews[viewIndex].steepBump = __ViewBump(x, y, \ |
|
|
|
activeViews[viewIndex].steepBump) |
|
|
|
|
|
|
|
curBump = activeViews[viewIndex].shallowBump |
|
|
|
while curBump is not None: |
|
|
|
if activeViews[viewIndex].steepLine.pBelow( \ |
|
|
|
curBump.x, curBump.y): |
|
|
|
activeViews[viewIndex].steepLine.xi = curBump.x |
|
|
|
activeViews[viewIndex].steepLine.yi = curBump.y |
|
|
|
|
|
|
|
curBump = curBump.parent |
|
|
|
|
|
|
|
def __checkView(activeViews, viewIndex): |
|
|
|
""" |
|
|
|
Removes the view in activeViews at index viewIndex if |
|
|
|
- The two lines are coolinear |
|
|
|
- The lines pass through either extremity |
|
|
|
""" |
|
|
|
|
|
|
|
shallowLine = activeViews[viewIndex].shallowLine |
|
|
|
steepLine = activeViews[viewIndex].steepLine |
|
|
|
|
|
|
|
if shallowLine.lineCollinear(steepLine) \ |
|
|
|
and ( shallowLine.pCollinear(0, 1) \ |
|
|
|
or shallowLine.pCollinear(1, 0) ): |
|
|
|
del activeViews[viewIndex] |
|
|
|
return False |
|
|
|
else: |
|
|
|
return True |
|
|
|
|