-- | DFOV (Digital Field of View) implemented according to specification at <http://roguebasin.roguelikedevelopment.org/index.php?title=Digital_field_of_view_implementation>.-- This fast version of the algorithm, based on "PFOV", has AFAIK-- never been described nor implemented before.moduleGame.LambdaHack.FOV.Digital(scan)whereimportGame.LambdaHack.MiscimportGame.LambdaHack.Utils.AssertimportGame.LambdaHack.FOV.Common-- | Calculates the list of tiles, in @Bump@ coordinates, visible from (0, 0),-- within the given sight range.scan::Distance-- ^ visiblity radius->(Bump->Bool)-- ^ clear tile predicate->[Bump]scanrisClear=-- The scanned area is a square, which is a sphere in the chessboard metric.dscan1(((B(1,0),B(-r,r)),[B(0,0)]),((B(0,0),B(r+1,r)),[B(1,0)]))wheredscan::Distance->EdgeInterval->[Bump]dscand(s0@(sl{-shallow line-},sBumps0),e@(el{-steep line-},eBumps))=letps0=let(n,k)=intersectsld-- minimal progress to considerinn`div`kpe=let(n,k)=intersecteld-- maximal progress to consider-- Corners obstruct view, so the steep line, constructed-- from corners, is itself not a part of the view,-- so if its intersection with the line of diagonals is only-- at a corner, choose the diamond leading to a smaller view.in-1+n`divUp`kinside=[B(p,d)|p<-[ps0..pe]]outside|d>=r=[]|isClear(B(ps0,d))=mscan(Justs0)(ps0+1)pe-- start in light|otherwise=mscanNothing(ps0+1)pe-- start in shadowinassert(r>=d&&d>=0&&pe>=ps0`blame`(r,d,s0,e,ps0,pe))$inside++outsidewhere-- The current state of a scan is kept in @Maybe Edge@.-- If it's the @Just@ case, we're in a visible interval. If @Nothing@,-- we're in a shadowed interval.mscan::MaybeEdge->Progress->Progress->[Bump]mscan(Justs@(_,sBumps))pspe|ps>pe=dscan(d+1)(s,e)-- reached end, scan next|not$isClearsteepBump=-- entering shadowmscanNothing(ps+1)pe++dscan(d+1)(s,(dlinenepsteepBump,neBumps))|otherwise=mscan(Justs)(ps+1)pe-- continue in lightwheresteepBump=B(ps,d)gte=dsteepersteepBumpnep=maximalgtesBumpsneBumps=addHullgtesteepBumpeBumpsmscanNothingpspe|ps>pe=[]-- reached end while in shadow|isClearshallowBump=-- moving out of shadowmscan(Just(dlinenspshallowBump,nsBumps))(ps+1)pe|otherwise=mscanNothing(ps+1)pe-- continue in shadowwhereshallowBump=B(ps,d)gte=flip$dsteepershallowBumpnsp=maximalgteeBumpsnsBumps=addHullgteshallowBumpsBumps0-- | Create a line from two points. Debug: check if well-defined.dline::Bump->Bump->Linedlinep1p2=assert(uncurryblame$debugLine(p1,p2))$(p1,p2)-- | Compare steepness of @(p1, f)@ and @(p2, f)@.-- Debug: Verify that the results of 2 independent checks are equal.dsteeper::Bump->Bump->Bump->Booldsteeperfp1p2=assert(res==debugSteeperfp1p2)$reswhereres=steeperfp1p2-- | The X coordinate, represented as a fraction, of the intersection of-- a given line and the line of diagonals of diamonds at distance-- @d@ from (0, 0).intersect::Line->Distance->(Int,Int)intersect(B(x,y),B(xf,yf))d=assert(allB(>=0)[y,yf])$((d-y)*(xf-x)+x*(yf-y),yf-y){-
Derivation of the formula:
The intersection point (xt, yt) satisfies the following equalities:
yt = d
(yt - y) (xf - x) = (xt - x) (yf - y)
hence
(yt - y) (xf - x) = (xt - x) (yf - y)
(d - y) (xf - x) = (xt - x) (yf - y)
(d - y) (xf - x) + x (yf - y) = xt (yf - y)
xt = ((d - y) (xf - x) + x (yf - y)) / (yf - y)
General remarks:
A diamond is denoted by its left corner. Hero at (0, 0).
Order of processing in the first quadrant rotated by 45 degrees is
45678
123
@
so the first processed diamond is at (-1, 1). The order is similar
as for the restrictive shadow casting algorithm and reversed wrt PFOV.
The line in the curent state of mscan is called the shallow line,
but it's the one that delimits the view from the left, while the steep
line is on the right, opposite to PFOV. We start scanning from the left.
The Point coordinates are cartesian. The Bump coordinates are cartesian,
translated so that the hero is at (0, 0) and rotated so that he always
looks at the first (rotated 45 degrees) quadrant. The (Progress, Distance)
cordinates coincide with the Bump coordinates, unlike in PFOV.
-}-- | Debug functions for DFOV:-- | Debug: calculate steeper for DFOV in another way and compare results.debugSteeper::Bump->Bump->Bump->BooldebugSteeperf@(B(_xf,yf))p1@(B(_x1,y1))p2@(B(_x2,y2))=assert(allB(>=0)[yf,y1,y2])$let(n1,k1)=intersect(p1,f)0(n2,k2)=intersect(p2,f)0inn1*k2>=k1*n2-- | Debug: check is a view border line for DFOV is legal.debugLine::Line->(Bool,String)debugLineline@(B(x1,y1),B(x2,y2))|not(allB(>=0)[y1,y2])=(False,"negative coordinates: "++showline)|y1==y2&&x1==x2=(False,"ill-defined line: "++showline)|y1==y2=(False,"horizontal line: "++showline)|crossL0=(False,"crosses the X axis below 0: "++showline)|crossG1=(False,"crosses the X axis above 1: "++showline)|otherwise=(True,"")where(n,k)=intersectline0(q,r)=ifk==0then(0,0)elsen`divMod`kcrossL0=q<0-- q truncated toward negative infinitycrossG1=q>=1&&(q>1||r/=0)