mirror of
https://github.com/mvvasilev/last_light.git
synced 2025-04-19 12:49:52 +03:00
87 lines
3.5 KiB
Go
87 lines
3.5 KiB
Go
package engine
|
|
|
|
import "math"
|
|
|
|
/* Stolen and modified from the go-fov package */
|
|
|
|
// Compute takes a GridMap implementation along with the x and y coordinates representing a player's current
|
|
// position and will internally update the visibile set of tiles within the provided radius `r`
|
|
func ComputeFOV[T any](transform func(x, y int) T, isInBounds, isOpaque func(x, y int) bool, px, py, radius int) (visibilityMap map[Position]T) {
|
|
visibilityMap = make(map[Position]T)
|
|
|
|
visibilityMap[PositionAt(px, py)] = transform(px, py)
|
|
|
|
for i := 1; i <= 8; i++ {
|
|
fov(visibilityMap, transform, isInBounds, isOpaque, px, py, 1, 0, 1, i, radius)
|
|
}
|
|
|
|
return visibilityMap
|
|
}
|
|
|
|
// fov does the actual work of detecting the visible tiles based on the recursive shadowcasting algorithm
|
|
// annotations provided inline below for (hopefully) easier learning
|
|
func fov[T any](visibilityMap map[Position]T, transform func(x, y int) T, isInBounds, isOpaque func(x, y int) bool, px, py, dist int, lowSlope, highSlope float64, oct, rad int) {
|
|
// If the current distance is greater than the radius provided, then this is the end of the iteration
|
|
if dist > rad {
|
|
return
|
|
}
|
|
|
|
// Convert our slope into integers that will represent the "height" from the player position
|
|
// "height" will alternately apply to x OR y coordinates as we move around the octants
|
|
low := math.Floor(lowSlope*float64(dist) + 0.5)
|
|
high := math.Floor(highSlope*float64(dist) + 0.5)
|
|
|
|
// inGap refers to whether we are currently scanning non-blocked tiles consecutively
|
|
// inGap = true means that the previous tile examined was empty
|
|
inGap := false
|
|
|
|
for height := low; height <= high; height++ {
|
|
// Given the player coords and a distance, height and octant, determine which tile is being visited
|
|
mapx, mapy := distHeightXY(px, py, dist, int(height), oct)
|
|
if isInBounds(mapx, mapy) && distTo(px, py, mapx, mapy) < rad {
|
|
// As long as a tile is within the bounds of the map, if we visit it at all, it is considered visible
|
|
// That's the efficiency of shadowcasting, you just dont visit tiles that aren't visible
|
|
visibilityMap[PositionAt(mapx, mapy)] = transform(mapx, mapy)
|
|
}
|
|
|
|
if isInBounds(mapx, mapy) && isOpaque(mapx, mapy) {
|
|
if inGap {
|
|
// An opaque tile was discovered, so begin a recursive call
|
|
fov(visibilityMap, transform, isInBounds, isOpaque, px, py, dist+1, lowSlope, (height-0.5)/float64(dist), oct, rad)
|
|
}
|
|
// Any time a recursive call is made, adjust the minimum slope for all future calls within this octant
|
|
lowSlope = (height + 0.5) / float64(dist)
|
|
inGap = false
|
|
} else {
|
|
inGap = true
|
|
// We've reached the end of the scan and, since the last tile in the scan was empty, begin
|
|
// another on the next depth up
|
|
if height == high {
|
|
fov(visibilityMap, transform, isInBounds, isOpaque, px, py, dist+1, lowSlope, highSlope, oct, rad)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// distHeightXY performs some bitwise and operations to handle the transposition of the depth and height values
|
|
// since the concept of "depth" and "height" is relative to whichever octant is currently being scanned
|
|
func distHeightXY(px, py, d, h, oct int) (int, int) {
|
|
if oct&0x1 > 0 {
|
|
d = -d
|
|
}
|
|
if oct&0x2 > 0 {
|
|
h = -h
|
|
}
|
|
if oct&0x4 > 0 {
|
|
return px + h, py + d
|
|
}
|
|
return px + d, py + h
|
|
}
|
|
|
|
// distTo is simply a helper function to determine the distance between two points, for checking visibility of a tile
|
|
// within a provided radius
|
|
func distTo(x1, y1, x2, y2 int) int {
|
|
vx := math.Pow(float64(x1-x2), 2)
|
|
vy := math.Pow(float64(y1-y2), 2)
|
|
return int(math.Sqrt(vx + vy))
|
|
}
|