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)