139 lines
4.3 KiB
GDScript
139 lines
4.3 KiB
GDScript
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)
|