Implement enemy AI and handler; add placeable radar object; enhance maze functionality
This commit is contained in:
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 574 B |
+24
-9
@@ -1,13 +1,18 @@
|
|||||||
class Character
|
class Character
|
||||||
attr_reader :world_x, :world_y
|
attr_reader :world_x, :world_y
|
||||||
|
|
||||||
SPEED = 2.5
|
SPEED = 3
|
||||||
SIZE = [25, 30]
|
SIZE = [25, 30]
|
||||||
|
|
||||||
def initialize
|
def initialize
|
||||||
@world_x = 60.0
|
@world_x = 60.0
|
||||||
@world_y = 60.0
|
@world_y = 60.0
|
||||||
|
|
||||||
|
start_room_coords = $bus.get(:start_room_coords)
|
||||||
|
if start_room_coords
|
||||||
|
@world_x, @world_y = [(start_room_coords[0] + 2) * 60 + 30, (start_room_coords[1] + 2) * 60 + 30]
|
||||||
|
end
|
||||||
|
|
||||||
# Load the full sheet as tiles
|
# Load the full sheet as tiles
|
||||||
sheet = Gosu::Image.load_tiles("assets/images/player.png", 20, 40, retro: true)
|
sheet = Gosu::Image.load_tiles("assets/images/player.png", 20, 40, retro: true)
|
||||||
|
|
||||||
@@ -31,6 +36,10 @@ class Character
|
|||||||
|
|
||||||
@current_animation = :idle_front
|
@current_animation = :idle_front
|
||||||
@facing = :front
|
@facing = :front
|
||||||
|
|
||||||
|
$bus.on_retrievable(:player_position) do
|
||||||
|
next [@world_x, @world_y]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def rect
|
def rect
|
||||||
@@ -79,16 +88,22 @@ class Character
|
|||||||
dx *= SPEED
|
dx *= SPEED
|
||||||
dy *= SPEED
|
dy *= SPEED
|
||||||
|
|
||||||
# Horizontal
|
steps = SPEED.ceil
|
||||||
@world_x += dx
|
step_dx = dx / steps.to_f
|
||||||
if $bus.get(:collides?, rect)&.include?(:wall)
|
step_dy = dy / steps.to_f
|
||||||
@world_x -= dx
|
|
||||||
|
steps.times do
|
||||||
|
# X axis
|
||||||
|
@world_x += step_dx
|
||||||
|
if ($bus.get(:collides?, rect) & [:wall, :object])&.any?
|
||||||
|
@world_x -= step_dx
|
||||||
end
|
end
|
||||||
|
|
||||||
# Vertical
|
# Y axis
|
||||||
@world_y += dy
|
@world_y += step_dy
|
||||||
if $bus.get(:collides?, rect)&.include?(:wall)
|
if ($bus.get(:collides?, rect) & [:wall, :object])&.any?
|
||||||
@world_y -= dy
|
@world_y -= step_dy
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# teleport to boss room for debug purposes
|
# teleport to boss room for debug purposes
|
||||||
|
|||||||
@@ -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
|
||||||
+6
-2
@@ -5,6 +5,10 @@ class Maze
|
|||||||
@maze = MazeData.new(80, 80)
|
@maze = MazeData.new(80, 80)
|
||||||
@spritesheet = Gosu::Image.load_tiles("assets/images/walls.png", 20, 40, retro: true)
|
@spritesheet = Gosu::Image.load_tiles("assets/images/walls.png", 20, 40, retro: true)
|
||||||
puts @spritesheet.size
|
puts @spritesheet.size
|
||||||
|
|
||||||
|
$bus.on_retrievable(:maze_solve) do |x1, y1, x2, y2|
|
||||||
|
next @maze.solve(x1, y1, x2, y2)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def collides?(rect)
|
def collides?(rect)
|
||||||
@@ -64,8 +68,8 @@ class Maze
|
|||||||
shapes
|
shapes
|
||||||
end
|
end
|
||||||
|
|
||||||
def draw(camera)
|
def draw
|
||||||
cam_x, cam_y = camera
|
cam_x, cam_y = $bus.get(:camera_pos) || [0, 0]
|
||||||
tile_size = 60
|
tile_size = 60
|
||||||
scale = 3
|
scale = 3
|
||||||
screen_width, screen_height = SCREEN_SIZE
|
screen_width, screen_height = SCREEN_SIZE
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
class Placeable
|
||||||
|
SIZE = [25, 15]
|
||||||
|
|
||||||
|
# The base class for all placeables in the game. Placeables are things that can be interacted with, but do not move (unlike characters).
|
||||||
|
# They cannot be walked through, but can be interacted with (e.g. a chest can be opened, a torch can be lit, a radar can be used).
|
||||||
|
# They are placed at the center of a tile, and their position is represented by the tile coordinate
|
||||||
|
# An object is one that is placed at the center of a tile (radar, torch & chest)
|
||||||
|
attr_reader :x, :y
|
||||||
|
|
||||||
|
def initialize(x, y)
|
||||||
|
@x = x
|
||||||
|
@y = y
|
||||||
|
@frames_count = 0
|
||||||
|
@spritesheet = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def collides?(rect)
|
||||||
|
return true if intersects?(self.rect, rect)
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def rect
|
||||||
|
[@x * 120 + 90 - SIZE[0] / 2, @y * 120 + 90 - SIZE[1], SIZE[0], SIZE[1]]
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
# For now, placeables don't have any behavior
|
||||||
|
end
|
||||||
|
|
||||||
|
def draw
|
||||||
|
return unless @spritesheet
|
||||||
|
|
||||||
|
cam_x, cam_y = $bus.get(:camera_pos) || [0, 0]
|
||||||
|
screen_x = @x * 120 + 90 - cam_x
|
||||||
|
screen_y = @y * 120 + 90 - cam_y
|
||||||
|
|
||||||
|
elapsed = Gosu.milliseconds / 1000.0
|
||||||
|
frame_index = ((elapsed * @frames_count).floor) % @frames_count
|
||||||
|
|
||||||
|
@spritesheet[frame_index].draw(screen_x - 20, screen_y - 20, screen_y, 2, 2)
|
||||||
|
|
||||||
|
return unless DEBUG
|
||||||
|
|
||||||
|
# collision box centered on same point
|
||||||
|
Gosu.draw_rect(screen_x - SIZE[0] / 2, screen_y - SIZE[1], SIZE[0], SIZE[1], Gosu::Color.new(0x88ff0000), Float::INFINITY)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Radar < Placeable
|
||||||
|
def initialize(x, y)
|
||||||
|
super(x, y)
|
||||||
|
@frames_count = 4
|
||||||
|
@spritesheet = Gosu::Image.load_tiles("assets/images/radar.png", 20, 20, retro: true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class ObjectHandler
|
||||||
|
attr_reader :objects
|
||||||
|
|
||||||
|
def initialize
|
||||||
|
start_room_coords = $bus.get(:start_room_coords)
|
||||||
|
@objects = [Radar.new(start_room_coords[0] / 2 - 1, start_room_coords[1] / 2 - 1)]
|
||||||
|
|
||||||
|
$bus.on_retrievable(:object_at) do |x, y|
|
||||||
|
next @objects.find { |obj| obj.x == x && obj.y == y }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def add(object)
|
||||||
|
@objects << object
|
||||||
|
end
|
||||||
|
|
||||||
|
def collides?(rect)
|
||||||
|
@objects.any? { |obj| obj.collides?(rect) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
@objects.each(&:update)
|
||||||
|
end
|
||||||
|
|
||||||
|
def draw
|
||||||
|
@objects.each(&:draw)
|
||||||
|
end
|
||||||
|
end
|
||||||
+21
-6
@@ -1,5 +1,7 @@
|
|||||||
require_relative 'state'
|
require_relative 'state'
|
||||||
require_relative 'maze'
|
require_relative 'maze'
|
||||||
|
require_relative 'placeables/base'
|
||||||
|
require_relative 'enemy/handler'
|
||||||
require_relative 'character'
|
require_relative 'character'
|
||||||
|
|
||||||
require 'perlin'
|
require 'perlin'
|
||||||
@@ -8,11 +10,13 @@ class Game < Scene
|
|||||||
def initialize
|
def initialize
|
||||||
super
|
super
|
||||||
@font = Gosu::Font.new(24)
|
@font = Gosu::Font.new(24)
|
||||||
@noise = Perlin::Generator.new(rand(1000), 1.0, 1)
|
@noise = Perlin::Generator.new(rand(1...1000), 1.0, 1)
|
||||||
@floor_image = Gosu::Image.new("assets/images/floor.png", retro: true)
|
@floor_image = Gosu::Image.new("assets/images/floor.png", retro: true)
|
||||||
|
|
||||||
@maze = Maze.new
|
@maze = Maze.new
|
||||||
@character = Character.new
|
@character = Character.new
|
||||||
|
@enemies = EnemyHandler.new
|
||||||
|
@objects = ObjectHandler.new
|
||||||
|
|
||||||
@camera = [0, 0]
|
@camera = [0, 0]
|
||||||
|
|
||||||
@@ -20,19 +24,27 @@ class Game < Scene
|
|||||||
@camera = pos
|
@camera = pos
|
||||||
end
|
end
|
||||||
|
|
||||||
|
$bus.on_retrievable(:camera_pos) do
|
||||||
|
next @camera
|
||||||
|
end
|
||||||
|
|
||||||
$bus.on_retrievable(:collides?) do |rect|
|
$bus.on_retrievable(:collides?) do |rect|
|
||||||
collisions = []
|
collisions = []
|
||||||
collisions << :wall if @maze.collides?(rect)
|
collisions << :wall if @maze.collides?(rect)
|
||||||
collisions << :character if @character.collides?(rect)
|
collisions << :character if @character.collides?(rect)
|
||||||
|
collisions << :enemy if @enemies.collides?(rect)
|
||||||
|
collisions << :object if @objects.collides?(rect)
|
||||||
next collisions
|
next collisions
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def draw
|
def draw
|
||||||
draw_floor
|
draw_floor
|
||||||
@maze.draw(@camera)
|
@maze.draw
|
||||||
@character.draw
|
@character.draw
|
||||||
draw_fog(@camera)
|
@enemies.draw
|
||||||
|
@objects.draw
|
||||||
|
draw_fog!
|
||||||
draw_debug! if DEBUG
|
draw_debug! if DEBUG
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -51,11 +63,12 @@ class Game < Scene
|
|||||||
end
|
end
|
||||||
|
|
||||||
@font.draw_text("FPS: #{@fps}", 5, 5, Float::INFINITY, 1, 1, Gosu::Color::YELLOW)
|
@font.draw_text("FPS: #{@fps}", 5, 5, Float::INFINITY, 1, 1, Gosu::Color::YELLOW)
|
||||||
@font.draw_text("Player: [#{@character.world_x.round}, #{@character.world_y.round}]", 5, 30, Float::INFINITY, 1, 1, Gosu::Color::YELLOW)
|
world_x, world_y = $bus.get(:player_position)&.map(&:round) || [0, 0]
|
||||||
|
@font.draw_text("Player: [#{world_x}, #{world_y}]", 5, 30, Float::INFINITY, 1, 1, Gosu::Color::YELLOW)
|
||||||
end
|
end
|
||||||
|
|
||||||
def draw_fog(camera)
|
def draw_fog!
|
||||||
cam_x, cam_y = camera
|
cam_x, cam_y = @camera
|
||||||
cell = 10
|
cell = 10
|
||||||
i_radius = 90.0
|
i_radius = 90.0
|
||||||
o_radius = 400.0
|
o_radius = 400.0
|
||||||
@@ -114,6 +127,8 @@ class Game < Scene
|
|||||||
|
|
||||||
def update
|
def update
|
||||||
@character.update
|
@character.update
|
||||||
|
@enemies.update
|
||||||
|
@objects.update
|
||||||
end
|
end
|
||||||
|
|
||||||
def button_down(id, _pos)
|
def button_down(id, _pos)
|
||||||
|
|||||||
+72
-6
@@ -1,16 +1,23 @@
|
|||||||
class MazeData
|
class MazeData
|
||||||
def initialize(width, height)
|
def initialize(width, height, standalone: false)
|
||||||
@width = width
|
@width = width
|
||||||
@height = height
|
@height = height
|
||||||
|
|
||||||
@grid = Array.new(height) { Array.new(width, 0) }
|
@grid = Array.new(height) { Array.new(width, 0) }
|
||||||
|
|
||||||
@boss_rooms = []
|
@boss_rooms = []
|
||||||
|
@start_room = nil
|
||||||
|
|
||||||
crate_rooms!
|
crate_rooms!
|
||||||
carve_passages_from(0, 0)
|
carve_passages_from(0, 0)
|
||||||
fix_room_entrances!
|
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
|
return unless DEBUG
|
||||||
|
|
||||||
print_debug
|
print_debug
|
||||||
@@ -19,6 +26,60 @@ class MazeData
|
|||||||
end
|
end
|
||||||
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
|
def width
|
||||||
@width * 2 + 1
|
@width * 2 + 1
|
||||||
end
|
end
|
||||||
@@ -114,7 +175,7 @@ class MazeData
|
|||||||
end
|
end
|
||||||
|
|
||||||
def fix_room_entrances!
|
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][x + 1] |= N
|
||||||
@grid[y - 1][x + 1] |= S
|
@grid[y - 1][x + 1] |= S
|
||||||
end
|
end
|
||||||
@@ -123,10 +184,11 @@ class MazeData
|
|||||||
def crate_rooms!
|
def crate_rooms!
|
||||||
base = Math.sqrt(@width * @height / 200.0).round
|
base = Math.sqrt(@width * @height / 200.0).round
|
||||||
num_rooms = rand((base - 1)..(base))
|
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
|
attempts = 0
|
||||||
|
rooms = []
|
||||||
|
|
||||||
while @boss_rooms.size < num_rooms && attempts < 10
|
while rooms.size < num_rooms && attempts < 10
|
||||||
attempts += 1
|
attempts += 1
|
||||||
|
|
||||||
x = rand(2..(@width - BOSS_ROOM_SIZE - 2))
|
x = rand(2..(@width - BOSS_ROOM_SIZE - 2))
|
||||||
@@ -148,8 +210,11 @@ class MazeData
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@boss_rooms << [x, y]
|
rooms << [x, y]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@start_room = rooms.shift
|
||||||
|
@boss_rooms = rooms
|
||||||
end
|
end
|
||||||
|
|
||||||
def carve_passages_from(cx, cy)
|
def carve_passages_from(cx, cy)
|
||||||
@@ -168,5 +233,6 @@ class MazeData
|
|||||||
end
|
end
|
||||||
|
|
||||||
if __FILE__ == $0
|
if __FILE__ == $0
|
||||||
|
maze = MazeData.new(80, 25, standalone: true)
|
||||||
|
maze.print_debug
|
||||||
end
|
end
|
||||||
@@ -10,3 +10,33 @@ def intersects?(rect1, rect2)
|
|||||||
|
|
||||||
true # overlap exists
|
true # overlap exists
|
||||||
end
|
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
|
||||||
Reference in New Issue
Block a user