bgen/demo/scripts/arcade/grid_util.gd
saarsena@gmail.com e45f121fb9 init
2026-04-16 21:04:50 -04:00

139 lines
4.3 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)