-- | A restrictive variant of Recursive Shadow Casting FOV with infinite range.-- It's not designed for dungeons with diagonal walls and so here-- they block visibility, though they don't block movement.-- The main advantage of the algorithm is that it's very simple and fast.moduleGame.LambdaHack.FOV.Shadow(SBump,Interval,scan)whereimportData.RatioimportGame.LambdaHack.Utils.AssertimportGame.LambdaHack.FOV.Common{-
Field Of View
-------------
The algorithm used is a variant of Shadow Casting. We first compute
fields that are reachable (have unobstructed line of sight) from the hero's
position. Later, in Perception.hs, from this information we compute
the fields that are visible (not hidden in darkness, etc.).
As input to the algorithm, we require information about fields that
block light. As output, we get information on the reachability of all fields.
We assume that the hero is located at position (0, 0)
and we only consider fields (line, row) where line >= 0 and 0 <= row <= line.
This is just about one eighth of the whole hero's surroundings,
but the other parts can be computed in the same fashion by mirroring
or rotating the given algorithm accordingly.
fov (blocks, maxline) =
shadow := \empty_set
reachable (0, 0) := True
for l \in [ 1 .. maxline ] do
for r \in [ 0 .. l ] do
reachable (l, r) := ( \exists a. a \in interval (l, r) \and
a \not_in shadow)
if blocks (l, r) then
shadow := shadow \union interval (l, r)
end if
end for
end for
return reachable
interval (l, r) = return [ angle (l + 0.5, r - 0.5),
angle (l - 0.5, r + 0.5) ]
angle (l, r) = return atan (r / l)
The algorithm traverses the fields line by line, row by row.
At every moment, we keep in shadow the intervals which are in shadow,
measured by their angle. A square is reachable when any point
in it is not in shadow --- the algorithm is permissive in this respect.
We could also require that a certain fraction of the field is reachable,
or a specific point. Our choice has certain consequences. For instance,
a single blocking field throws a shadow, but the fields immediately behind
the blocking field are still visible.
We can compute the interval of angles corresponding to one square field
by computing the angle of the line passing the upper left corner
and the angle of the line passing the lower right corner.
This is what interval and angle do. If a field is blocking, the interval
for the square is added to the shadow set.
-}-- | Rotated and translated coordinates of 2D points, so that they fit-- in the same single octant area.typeSBump=(Progress,Distance)-- | The area left to be scanned, delimited by fractions of the original arc.-- Interval @(0, 1)@ means the whole 45 degrees arc of the processed octant-- is to be scanned.typeInterval=(Rational,Rational)-- TODO: if every used, apply static argument transformation to isClear.-- | Calculates the list of tiles, in @SBump@ coordinates, visible from (0, 0).scan::(SBump->Bool)-- ^ clear tile predicate->Distance-- ^ the current distance from (0, 0)->Interval-- ^ the current interval to scan->[SBump]scanisCleard(s0,e)=letps=downBias(s0*fromIntegrald)-- minimal progress to considerpe=upBias(e*fromIntegrald)-- maximal progress to considerinside=[(p,d)|p<-[ps..pe]]outside|isClear(ps,d)=mscan(Justs0)pspe-- start in light|otherwise=mscanNothingpspe-- start in shadowinassert(d>=0&&e>=0&&s0>=0&&pe>=ps&&ps>=0`blame`(d,s0,e,ps,pe))$inside++outsidewhere-- The current state of a scan is kept in @Maybe Rational@.-- If it's the @Just@ case, we're in a visible interval. If @Nothing@,-- we're in a shadowed interval.mscan::MaybeRational->Progress->Progress->[SBump]mscan(Justs)pspe|s>=e=[]-- empty interval|ps>pe=scanisClear(d+1)(s,e)-- reached end, scan next|not$isClear(ps,d)=-- entering shadowletne=(fromIntegralps-(1%2))/(fromIntegrald+(1%2))inmscanNothing(ps+1)pe++scanisClear(d+1)(s,ne)|otherwise=mscan(Justs)(ps+1)pe-- continue in lightmscanNothingpspe|ps>pe=[]-- reached end while in shadow|isClear(ps,d)=-- moving out of shadowletns=(fromIntegralps-(1%2))/(fromIntegrald-(1%2))inmscan(Justns)(ps+1)pe|otherwise=mscanNothing(ps+1)pe-- continue in shadowdownBias,upBias::(Integrala,Integralb)=>Ratioa->bdownBiasx=round(x-1%(denominatorx*3))upBiasx=round(x+1%(denominatorx*3))