123 lines
3.6 KiB
GDScript
123 lines
3.6 KiB
GDScript
class_name BrogueFOV
|
|
extends RefCounted
|
|
|
|
# Symmetric recursive shadowcasting, tile-accurate FOV for 2D grids.
|
|
#
|
|
# Port of the standard 8-octant algorithm (Bjoern Bergstrom / Adam Milazzo's
|
|
# symmetric variant). In ~100 lines of pure GDScript.
|
|
#
|
|
# Properties:
|
|
# - Tile-accurate: every cell is either fully visible or not visible.
|
|
# - Symmetric: if A sees B, B sees A. Critical for fair AI awareness.
|
|
# - Circular radius clipping (euclidean distance).
|
|
# - Opaque = walls, T_NOTHING, and unbridged liquid (lava/chasm/brimstone).
|
|
# Water is transparent (you can see across water).
|
|
#
|
|
# Usage:
|
|
# var vis := BrogueFOV.compute(grid, Vector2i(39, 14), 8)
|
|
# for y in grid["height"]:
|
|
# for x in grid["width"]:
|
|
# if vis[y * grid["width"] + x] == 1:
|
|
# draw_cell(x, y)
|
|
|
|
# Must match src/gen/grid.h:terrain_t
|
|
const T_NOTHING := 0
|
|
const T_FLOOR := 1
|
|
const T_WALL := 2
|
|
const T_DOOR := 3
|
|
const T_CORRIDOR := 4
|
|
const T_LIQUID := 5
|
|
const T_BRIDGE := 6
|
|
const T_STAIRS_UP := 7
|
|
const T_STAIRS_DOWN := 8
|
|
|
|
const L_WATER := 1
|
|
|
|
# Octant transforms: each octant maps (row, col) of the shadowcasting loop
|
|
# to grid deltas. 8 octants cover all directions symmetrically.
|
|
const OCTANTS := [
|
|
Vector4i( 1, 0, 0, 1),
|
|
Vector4i( 0, 1, 1, 0),
|
|
Vector4i( 0, -1, 1, 0),
|
|
Vector4i(-1, 0, 0, 1),
|
|
Vector4i(-1, 0, 0, -1),
|
|
Vector4i( 0, -1, -1, 0),
|
|
Vector4i( 0, 1, -1, 0),
|
|
Vector4i( 1, 0, 0, -1),
|
|
]
|
|
|
|
# Returns PackedByteArray of width*height, row-major, 1 = visible.
|
|
static func compute(grid: Dictionary, origin: Vector2i, radius: int) -> PackedByteArray:
|
|
var w: int = grid["width"]
|
|
var h: int = grid["height"]
|
|
var out := PackedByteArray()
|
|
out.resize(w * h)
|
|
for i in range(w * h):
|
|
out[i] = 0
|
|
|
|
if origin.x < 0 or origin.y < 0 or origin.x >= w or origin.y >= h:
|
|
return out
|
|
|
|
# Origin is always visible.
|
|
out[origin.y * w + origin.x] = 1
|
|
|
|
var terrain: PackedByteArray = grid["terrain"]
|
|
var liquid: PackedByteArray = grid["liquid"]
|
|
|
|
for oct in OCTANTS:
|
|
_cast_octant(out, terrain, liquid, origin, radius, w, h,
|
|
1, 1.0, 0.0, oct)
|
|
return out
|
|
|
|
static func _is_opaque(terrain: PackedByteArray, liquid: PackedByteArray,
|
|
x: int, y: int, w: int) -> bool:
|
|
var idx := y * w + x
|
|
var t: int = terrain[idx]
|
|
if t == T_WALL or t == T_NOTHING:
|
|
return true
|
|
if t == T_LIQUID:
|
|
# Water is transparent; lava / chasm / brimstone are opaque.
|
|
return liquid[idx] != L_WATER
|
|
return false
|
|
|
|
static func _cast_octant(out: PackedByteArray,
|
|
terrain: PackedByteArray, liquid: PackedByteArray,
|
|
origin: Vector2i, radius: int, w: int, h: int,
|
|
row: int, start_slope: float, end_slope: float,
|
|
oct: Vector4i) -> void:
|
|
if start_slope < end_slope:
|
|
return
|
|
var r2: int = radius * radius
|
|
var new_start := start_slope
|
|
var blocked := false
|
|
for i in range(row, radius + 1):
|
|
if blocked:
|
|
break
|
|
for dy in range(-i, 1):
|
|
var dx := -i
|
|
while dx <= 0:
|
|
var tx: int = origin.x + dx * oct.x + dy * oct.y
|
|
var ty: int = origin.y + dx * oct.z + dy * oct.w
|
|
var l_slope := (dx - 0.5) / (dy + 0.5)
|
|
var r_slope := (dx + 0.5) / (dy - 0.5)
|
|
if start_slope < r_slope:
|
|
dx += 1
|
|
continue
|
|
if end_slope > l_slope:
|
|
break
|
|
if tx >= 0 and tx < w and ty >= 0 and ty < h:
|
|
if dx * dx + dy * dy <= r2:
|
|
out[ty * w + tx] = 1
|
|
if blocked:
|
|
if _is_opaque(terrain, liquid, tx, ty, w):
|
|
new_start = r_slope
|
|
else:
|
|
blocked = false
|
|
start_slope = new_start
|
|
else:
|
|
if _is_opaque(terrain, liquid, tx, ty, w) and i < radius:
|
|
blocked = true
|
|
_cast_octant(out, terrain, liquid, origin, radius, w, h,
|
|
i + 1, start_slope, l_slope, oct)
|
|
new_start = r_slope
|
|
dx += 1
|