Implement enemy AI and handler; add placeable radar object; enhance maze functionality

This commit is contained in:
2026-03-25 22:02:50 +00:00
parent 84004a9f67
commit 83d250f1e9
13 changed files with 478 additions and 24 deletions
+72 -6
View File
@@ -1,16 +1,23 @@
class MazeData
def initialize(width, height)
def initialize(width, height, standalone: false)
@width = width
@height = height
@grid = Array.new(height) { Array.new(width, 0) }
@boss_rooms = []
@start_room = nil
crate_rooms!
carve_passages_from(0, 0)
fix_room_entrances!
return if standalone
$bus.on_retrievable(:start_room_coords) do
next @start_room ? [@start_room[0] * 2 + 1, @start_room[1] * 2 + 1] : nil
end
return unless DEBUG
print_debug
@@ -19,6 +26,60 @@ class MazeData
end
end
def solve(x1, y1, x2, y2)
# explain why dijkstra's is fine here:
# A* hueristics make it possible to chose a longer path through rooms instead of a shorter path through corridors, which is not what we want for enemy pathfinding
# Dijkstra's is also simpler to implement since we don't need to worry about the heuristic function, and the maze is not large enough for performance to be a concern
distances = Array.new(@height) { Array.new(@width, Float::INFINITY) }
visited = Array.new(@height) { Array.new(@width, false) }
previous = Array.new(@height) { Array.new(@width, nil) }
distances[y1][x1] = 0
queue = [[y1, x1]]
while !queue.empty?
cy, cx = queue.shift
next if visited[cy][cx]
visited[cy][cx] = true
return build_path(previous, x1, y1, x2, y2) if cx == x2 && cy == y2
# can check NSEW walls here to determine which neighbors to add to the queue (no need to check teh neighbors)
[N, S, E, W].each do |direction|
next if (@grid[cy][cx] & direction) == 0 # wall in this direction
nx, ny = cx + DX[direction], cy + DY[direction]
next unless ny.between?(0, @height - 1) && nx.between?(0, @width - 1)
alt = distances[cy][cx] + 1
if alt < distances[ny][nx]
distances[ny][nx] = alt
previous[ny][nx] = [cx, cy]
queue << [ny, nx]
end
end
end
return nil
end
def build_path(previous, x1, y1, x2, y2)
path = []
cx, cy = x2, y2
while cx != x1 || cy != y1
return nil unless previous[cy][cx]
path << [cx, cy]
cx, cy = previous[cy][cx]
end
path << [x1, y1]
path.reverse!
end
def width
@width * 2 + 1
end
@@ -114,7 +175,7 @@ class MazeData
end
def fix_room_entrances!
@boss_rooms.each do |x, y|
(@boss_rooms + [@start_room]).each do |x, y|
@grid[y][x + 1] |= N
@grid[y - 1][x + 1] |= S
end
@@ -123,10 +184,11 @@ class MazeData
def crate_rooms!
base = Math.sqrt(@width * @height / 200.0).round
num_rooms = rand((base - 1)..(base))
num_rooms = 1 if num_rooms < 1
num_rooms = (num_rooms < 1 ? 1 : num_rooms) + 1 # for start room
attempts = 0
rooms = []
while @boss_rooms.size < num_rooms && attempts < 10
while rooms.size < num_rooms && attempts < 10
attempts += 1
x = rand(2..(@width - BOSS_ROOM_SIZE - 2))
@@ -148,8 +210,11 @@ class MazeData
end
end
@boss_rooms << [x, y]
rooms << [x, y]
end
@start_room = rooms.shift
@boss_rooms = rooms
end
def carve_passages_from(cx, cy)
@@ -168,5 +233,6 @@ class MazeData
end
if __FILE__ == $0
maze = MazeData.new(80, 25, standalone: true)
maze.print_debug
end
+30
View File
@@ -9,4 +9,34 @@ def intersects?(rect1, rect2)
return false if y1 >= y2 + h2 # rect1 is below rect2
true # overlap exists
end
def line_of_sight?(x1, y1, x2, y2, rect)
rx, ry, rw, rh = rect
rx, ry, rw, rh = rx - 2, ry - 2, rw + 4, rh + 4
# The 4 edges of the rect as line segments
edges = [
[rx, ry, rx + rw, ry ], # top
[rx, ry + rh, rx + rw, ry + rh], # bottom
[rx, ry, rx, ry + rh], # left
[rx + rw, ry, rx + rw, ry + rh], # right
]
!(edges.any? { |ex1, ey1, ex2, ey2| segments_intersect?(x1, y1, x2, y2, ex1, ey1, ex2, ey2) })
end
def segments_intersect?(ax1, ay1, ax2, ay2, bx1, by1, bx2, by2)
dx1 = ax2 - ax1
dy1 = ay2 - ay1
dx2 = bx2 - bx1
dy2 = by2 - by1
denom = dx1 * dy2 - dy1 * dx2
return false if denom == 0 # parallel
t = ((bx1 - ax1) * dy2 - (by1 - ay1) * dx2).to_f / denom
u = ((bx1 - ax1) * dy1 - (by1 - ay1) * dx1).to_f / denom
t.between?(0, 1) && u.between?(0, 1)
end