Implement enemy AI and handler; add placeable radar object; enhance maze functionality
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
class EnemyAI
|
||||
AGGRO_RADIUS = 2000
|
||||
SPEED = 2
|
||||
CORNER_OFFSET = 35 # how far from tile center to place corner waypoint
|
||||
|
||||
def initialize(enemy, type)
|
||||
@enemy = enemy
|
||||
@type = type
|
||||
@tile_path = [] # raw tile coords from solver
|
||||
@waypoints = [] # actual world positions to move through
|
||||
@last_player_tile = nil
|
||||
@avoiding_obstacle = false
|
||||
end
|
||||
|
||||
def update(player_x, player_y)
|
||||
distance = Gosu.distance(@enemy.x, @enemy.y, player_x, player_y)
|
||||
return unless distance < AGGRO_RADIUS
|
||||
|
||||
player_tile = [(player_x - 90) / 120, (player_y - 90) / 120].map(&:round)
|
||||
enemy_tile = [(@enemy.x - 90) / 120, (@enemy.y - 90) / 120].map(&:round)
|
||||
|
||||
return if $bus.get(:collides?, @enemy.rect)&.include?(:character)
|
||||
|
||||
object_in_tile = $bus.get(:object_at, enemy_tile[0], enemy_tile[1])
|
||||
|
||||
if player_tile == enemy_tile
|
||||
los_blocked = !object_in_tile.nil? && !(
|
||||
line_of_sight?(@enemy.x - @enemy.w / 2, @enemy.y - @enemy.h / 2, player_x, player_y, object_in_tile.rect) &&
|
||||
line_of_sight?(@enemy.x + @enemy.w / 2, @enemy.y + @enemy.h / 2, player_x, player_y, object_in_tile.rect) &&
|
||||
line_of_sight?(@enemy.x - @enemy.w / 2, @enemy.y + @enemy.h / 2, player_x, player_y, object_in_tile.rect) &&
|
||||
line_of_sight?(@enemy.x + @enemy.w / 2, @enemy.y - @enemy.h / 2, player_x, player_y, object_in_tile.rect)
|
||||
)
|
||||
|
||||
if los_blocked
|
||||
if !@avoiding_obstacle
|
||||
center_x = enemy_tile[0] * 120 + 90
|
||||
center_y = enemy_tile[1] * 120 + 90
|
||||
|
||||
corners = [
|
||||
[center_x - CORNER_OFFSET, center_y - CORNER_OFFSET],
|
||||
[center_x + CORNER_OFFSET, center_y - CORNER_OFFSET],
|
||||
[center_x + CORNER_OFFSET, center_y + CORNER_OFFSET],
|
||||
[center_x - CORNER_OFFSET, center_y + CORNER_OFFSET]
|
||||
]
|
||||
|
||||
closest_idx = corners.each_with_index.min_by { |c, _| Gosu.distance(c[0], c[1], @enemy.x, @enemy.y) }[1]
|
||||
@waypoints = corners.rotate(closest_idx)
|
||||
@avoiding_obstacle = true
|
||||
end
|
||||
else
|
||||
move_towards(player_x, player_y)
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
if @waypoints.empty? || @last_player_tile != player_tile
|
||||
@last_player_tile = player_tile
|
||||
@tile_path = $bus.get(:maze_solve, enemy_tile[0], enemy_tile[1], player_tile[0], player_tile[1]) || []
|
||||
if $bus.get(:object_at, enemy_tile[0], enemy_tile[1]).nil?
|
||||
@tile_path.shift
|
||||
end
|
||||
@waypoints = build_waypoints(@tile_path)
|
||||
end
|
||||
|
||||
return if @waypoints.empty?
|
||||
|
||||
target_x, target_y = @waypoints.first
|
||||
|
||||
if intersects?(@enemy.rect, [target_x - 5, target_y - 5, 10, 10])
|
||||
@waypoints.shift
|
||||
@avoiding_obstacle = false if @waypoints.empty?
|
||||
return
|
||||
end
|
||||
|
||||
move_towards(target_x, target_y)
|
||||
end
|
||||
|
||||
def draw
|
||||
return unless DEBUG
|
||||
cam_x, cam_y = $bus.get(:camera_pos) || [0, 0]
|
||||
@waypoints.each do |wx, wy|
|
||||
Gosu.draw_rect(wx - cam_x - 5, wy - cam_y - 5, 10, 10, Gosu::Color.new(0x88ffff00), Float::INFINITY)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build_waypoints(tile_path)
|
||||
waypoints = []
|
||||
|
||||
tile_path.each_with_index do |(tx, ty), i|
|
||||
center_x = tx * 120 + 90
|
||||
center_y = ty * 120 + 90
|
||||
|
||||
unless $bus.get(:object_at, tx, ty).nil?
|
||||
next_tile = tile_path[i + 1]
|
||||
prev_tile = i > 0 ? tile_path[i - 1] : nil
|
||||
|
||||
next_cx = next_tile ? next_tile[0] * 120 + 90 : center_x
|
||||
next_cy = next_tile ? next_tile[1] * 120 + 90 : center_y
|
||||
prev_cx = prev_tile ? prev_tile[0] * 120 + 90 : @enemy.x
|
||||
prev_cy = prev_tile ? prev_tile[1] * 120 + 90 : @enemy.y
|
||||
|
||||
corners = [
|
||||
[center_x - CORNER_OFFSET, center_y - CORNER_OFFSET],
|
||||
[center_x + CORNER_OFFSET, center_y - CORNER_OFFSET],
|
||||
[center_x - CORNER_OFFSET, center_y + CORNER_OFFSET],
|
||||
[center_x + CORNER_OFFSET, center_y + CORNER_OFFSET]
|
||||
]
|
||||
|
||||
# first waypoint: corner closest to where we're coming from
|
||||
first = corners.min_by { |cx, cy| Gosu.distance(cx, cy, prev_cx, prev_cy) }
|
||||
|
||||
# second waypoint: closest to next tile but not opposite to first
|
||||
opposite = [center_x - (first[0] - center_x), center_y - (first[1] - center_y)]
|
||||
candidates = corners.reject { |c| c == first || c == opposite }
|
||||
second = candidates.min_by { |cx, cy| Gosu.distance(cx, cy, next_cx, next_cy) }
|
||||
|
||||
waypoints << first
|
||||
waypoints << second
|
||||
else
|
||||
waypoints << [center_x, center_y]
|
||||
end
|
||||
end
|
||||
|
||||
waypoints
|
||||
end
|
||||
|
||||
def move_towards(target_x, target_y)
|
||||
angle = Gosu.angle(@enemy.x, @enemy.y, target_x, target_y)
|
||||
dx = Gosu.offset_x(angle, SPEED)
|
||||
dy = Gosu.offset_y(angle, SPEED)
|
||||
|
||||
steps = SPEED.ceil
|
||||
step_dx = dx / steps.to_f
|
||||
step_dy = dy / steps.to_f
|
||||
|
||||
steps.times do
|
||||
moved_x = false
|
||||
moved_y = false
|
||||
|
||||
# Try X
|
||||
@enemy.x += step_dx
|
||||
if ($bus.get(:collides?, @enemy.rect) & [:wall, :object])&.any?
|
||||
@enemy.x -= step_dx
|
||||
else
|
||||
moved_x = true
|
||||
end
|
||||
|
||||
# Try Y (independent — keeps full intent)
|
||||
@enemy.y += step_dy
|
||||
if ($bus.get(:collides?, @enemy.rect) & [:wall, :object])&.any?
|
||||
@enemy.y -= step_dy
|
||||
else
|
||||
moved_y = true
|
||||
end
|
||||
|
||||
# If both failed, stop early
|
||||
break unless moved_x || moved_y
|
||||
end
|
||||
|
||||
if $bus.get(:collides?, @enemy.rect)&.include?(:character)
|
||||
# Attack player
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,46 @@
|
||||
require_relative 'ai'
|
||||
|
||||
class Enemy
|
||||
attr_accessor :x, :y, :w, :h
|
||||
|
||||
def initialize(x, y)
|
||||
@x = x
|
||||
@y = y
|
||||
@w = 25
|
||||
@h = 30
|
||||
|
||||
@ai = EnemyAI.new(self, :chase)
|
||||
end
|
||||
|
||||
def rect
|
||||
[@x - @w / 2, @y - @h / 2, @w, @h]
|
||||
end
|
||||
|
||||
def collides?(rect)
|
||||
return true if intersects?(self.rect, rect)
|
||||
false
|
||||
end
|
||||
|
||||
def update
|
||||
player_pos = $bus.get(:player_position)
|
||||
@ai.update(*player_pos) if player_pos
|
||||
end
|
||||
|
||||
def draw
|
||||
cam_x, cam_y = $bus.get(:camera_pos) || [0, 0]
|
||||
|
||||
screen_x = @x - cam_x
|
||||
screen_y = @y - cam_y
|
||||
|
||||
Gosu.draw_rect(
|
||||
screen_x - @w / 2,
|
||||
screen_y - @h / 2,
|
||||
@w,
|
||||
@h,
|
||||
Gosu::Color::RED,
|
||||
screen_y - @h / 2
|
||||
)
|
||||
|
||||
@ai.draw
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,28 @@
|
||||
require_relative 'base'
|
||||
|
||||
class EnemyHandler
|
||||
def initialize
|
||||
@enemies = []
|
||||
end
|
||||
|
||||
def update
|
||||
spawn!
|
||||
@enemies.each(&:update)
|
||||
end
|
||||
|
||||
def spawn!
|
||||
# For now, just spawn a single enemy at a fixed location
|
||||
start_room_coords = $bus.get(:start_room_coords)
|
||||
if @enemies.empty?
|
||||
@enemies << Enemy.new((start_room_coords[0] + 4) * 60 + 30, (start_room_coords[1] + 4) * 60 + 30)
|
||||
end
|
||||
end
|
||||
|
||||
def draw
|
||||
@enemies.each(&:draw)
|
||||
end
|
||||
|
||||
def collides?(rect)
|
||||
@enemies.any? { |enemy| enemy.collides?(rect) }
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user