This commit is contained in:
saarsena@gmail.com 2026-04-16 21:04:50 -04:00
commit e45f121fb9
89 changed files with 336069 additions and 0 deletions

View file

@ -0,0 +1,139 @@
class_name GridUtil
extends RefCounted
# Static helpers for the arcade game. Single 79×29 grid per level; the
# generator dict is the source of truth. World↔grid mapping uses 1-unit
# cells (matches demo_fps.gd GridMap cell_size).
#
# Pathfinding is a plain 4-connected BFS, tiny code, fast enough at this
# scale (<1 ms for a full grid flood on a laptop). For an arcade game we
# recompute per-enemy every ~200ms, not every frame.
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
const L_LAVA := 2
const L_CHASM := 3
const L_BRIMSTONE := 4
const CELL_SIZE := 1.0
# --- coordinate mapping ---
static func grid_to_world(g: Vector2i, y: float = 0.0) -> Vector3:
return Vector3(float(g.x), y, float(g.y))
static func world_to_grid(w: Vector3) -> Vector2i:
return Vector2i(int(floor(w.x + 0.5)), int(floor(w.z + 0.5)))
# --- passability ---
# A cell is passable if an enemy (or player, for pathfinding intent) can
# step on it. Corridors, floor, doors, bridges, stairs, water count.
# Walls, chasms, lava, brimstone do not.
static func is_passable(grid: Dictionary, pos: Vector2i) -> bool:
var w: int = grid["width"]
var h: int = grid["height"]
if pos.x < 0 or pos.y < 0 or pos.x >= w or pos.y >= h:
return false
var idx := pos.y * w + pos.x
var t: int = (grid["terrain"] as PackedByteArray)[idx]
if t == T_LIQUID:
var liq: int = (grid["liquid"] as PackedByteArray)[idx]
return liq == L_WATER # water walkable, lava/chasm/brimstone not
return t == T_FLOOR or t == T_CORRIDOR or t == T_DOOR \
or t == T_BRIDGE or t == T_STAIRS_UP or t == T_STAIRS_DOWN
# --- BFS pathfinding ---
# Returns the next grid step from `from` toward `to`, or from itself if no
# path exists or we're already there. 4-connected, uniform cost.
static func next_step_toward(grid: Dictionary, from: Vector2i, to: Vector2i) -> Vector2i:
if from == to:
return from
if not is_passable(grid, from) or not is_passable(grid, to):
return from
var w: int = grid["width"]
var h: int = grid["height"]
var came_from := {} # Vector2i → Vector2i
came_from[from] = from
var queue: Array[Vector2i] = [from]
var found := false
var dirs := [Vector2i(1,0), Vector2i(-1,0), Vector2i(0,1), Vector2i(0,-1)]
while queue.size() > 0 and not found:
var cur: Vector2i = queue.pop_front()
for d in dirs:
var nxt: Vector2i = cur + d
if came_from.has(nxt): continue
if not is_passable(grid, nxt): continue
came_from[nxt] = cur
if nxt == to:
found = true
break
queue.push_back(nxt)
if not found:
return from
# Walk back from to → find cell whose came_from is `from`
var cur: Vector2i = to
while came_from[cur] != from:
cur = came_from[cur]
return cur
# True if there's an unobstructed line from a → b for enemy LOS purposes.
# Walks the Bresenham ray; any non-passable cell (wall / chasm / lava)
# blocks sight. Doors are transparent for LOS.
static func has_los(grid: Dictionary, a: Vector2i, b: Vector2i) -> bool:
var dx := absi(b.x - a.x)
var dy := absi(b.y - a.y)
var sx := 1 if a.x < b.x else -1
var sy := 1 if a.y < b.y else -1
var err := dx - dy
var x := a.x
var y := a.y
while true:
if Vector2i(x, y) == b: return true
# Skip blocking check at start cell.
if not (x == a.x and y == a.y):
if not _transparent(grid, Vector2i(x, y)):
return false
var e2 := 2 * err
if e2 > -dy:
err -= dy
x += sx
if e2 < dx:
err += dx
y += sy
return false
static func _transparent(grid: Dictionary, pos: Vector2i) -> bool:
var w: int = grid["width"]
var h: int = grid["height"]
if pos.x < 0 or pos.y < 0 or pos.x >= w or pos.y >= h:
return false
var idx := pos.y * w + pos.x
var t: int = (grid["terrain"] as PackedByteArray)[idx]
# Walls block; everything else is transparent for LOS.
return t != T_WALL and t != 0
# Pick a random passable cell in the given list of cells. Returns (-1,-1)
# if none are passable. Used by enemy wander target selection.
static func random_passable(grid: Dictionary, cells: Array, rng: RandomNumberGenerator) -> Vector2i:
if cells.is_empty():
return Vector2i(-1, -1)
var shuffled := cells.duplicate()
shuffled.shuffle()
for c in shuffled:
if is_passable(grid, c):
return c
return Vector2i(-1, -1)