140 lines
4.3 KiB
GDScript3
140 lines
4.3 KiB
GDScript3
|
|
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)
|