Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
da90a7836e
|
|||
| 3d4ae43f52 | |||
| 35cddfc53c | |||
| bf1171a775 | |||
| 5814a1fdee | |||
| 222b28d196 | |||
| b325b35c88 | |||
| 94be1f104f | |||
| d0466599df | |||
| 7c35419026 |
@@ -1 +1,3 @@
|
|||||||
*.old.*
|
*.old.*
|
||||||
|
config.json
|
||||||
|
p.vim
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"editor.tabSize": 2,
|
|
||||||
"editor.insertSpaces": true,
|
|
||||||
"editor.fontFamily": "'Operator Mono Lig', monospace",
|
|
||||||
"editor.fontSize": 14,
|
|
||||||
"editor.fontLigatures": true,
|
|
||||||
}
|
|
||||||
|
After Width: | Height: | Size: 233 B |
|
After Width: | Height: | Size: 352 B |
|
After Width: | Height: | Size: 162 B |
|
After Width: | Height: | Size: 505 B |
|
Before Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 187 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 453 B |
|
After Width: | Height: | Size: 186 B |
|
After Width: | Height: | Size: 386 B |
|
After Width: | Height: | Size: 367 B |
@@ -0,0 +1,44 @@
|
|||||||
|
class Credits < Scene
|
||||||
|
def initialize
|
||||||
|
super
|
||||||
|
@font = Gosu::Font.new(32, name: "assets/fonts/tn.ttf")
|
||||||
|
@small_font = Gosu::Font.new(20, name: "assets/fonts/tn.ttf")
|
||||||
|
end
|
||||||
|
|
||||||
|
def update(_dt)
|
||||||
|
# nothing needed
|
||||||
|
end
|
||||||
|
|
||||||
|
def draw
|
||||||
|
Gosu.draw_rect(0, 0, *SCREEN_SIZE, Gosu::Color.new(0xFF131313))
|
||||||
|
|
||||||
|
# title (centered)
|
||||||
|
title = "Credits"
|
||||||
|
tx = (720 - @font.text_width(title)) / 2
|
||||||
|
@font.draw_text(title, tx, 80, 1, 1, 1, Gosu::Color::YELLOW)
|
||||||
|
|
||||||
|
# main content
|
||||||
|
# this is kinda useless and i could just hardcode the positions, or even the image itself but yeah
|
||||||
|
lines = [
|
||||||
|
"Developer: Syed Daanish",
|
||||||
|
"Artist: Syed Daanish"
|
||||||
|
]
|
||||||
|
|
||||||
|
lines.each_with_index do |line, i|
|
||||||
|
x = (720 - @small_font.text_width(line)) / 2
|
||||||
|
y = 180 + i * 40
|
||||||
|
@small_font.draw_text(line, x, y, 1, 1, 1, Gosu::Color::WHITE)
|
||||||
|
end
|
||||||
|
|
||||||
|
# hint at bottom
|
||||||
|
hint = "Press ESC to return"
|
||||||
|
hx = (720 - @small_font.text_width(hint)) / 2
|
||||||
|
@small_font.draw_text(hint, hx, 420, 1, 1, 1, Gosu::Color::GRAY)
|
||||||
|
end
|
||||||
|
|
||||||
|
def button_down(id, _pos)
|
||||||
|
if id == Gosu::KB_ESCAPE || id == Gosu::KB_RETURN
|
||||||
|
$bus.emit(:change_scene, Menu)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -76,10 +76,10 @@ class Character
|
|||||||
dx = 0.0
|
dx = 0.0
|
||||||
dy = 0.0
|
dy = 0.0
|
||||||
|
|
||||||
dx -= 1 if Gosu.button_down?(Gosu::KB_A)
|
dx -= 1 if Gosu.button_down?($bus.get(:settings, :leftward))
|
||||||
dx += 1 if Gosu.button_down?(Gosu::KB_D)
|
dx += 1 if Gosu.button_down?($bus.get(:settings, :rightward))
|
||||||
dy -= 1 if Gosu.button_down?(Gosu::KB_W)
|
dy -= 1 if Gosu.button_down?($bus.get(:settings, :forward))
|
||||||
dy += 1 if Gosu.button_down?(Gosu::KB_S)
|
dy += 1 if Gosu.button_down?($bus.get(:settings, :backward))
|
||||||
|
|
||||||
moving = dx != 0 || dy != 0
|
moving = dx != 0 || dy != 0
|
||||||
|
|
||||||
@@ -122,11 +122,15 @@ class Character
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# teleport to boss room for debug purposes
|
if $bus.get_all(:collides?, rect).include?(:trap)
|
||||||
|
$bus.emit(:trap_stepped_on, rect, :character)
|
||||||
|
end
|
||||||
|
|
||||||
|
# teleport to exit room for debug purposes
|
||||||
if $bus.get(:settings, :debug) && Gosu.button_down?(Gosu::KB_T)
|
if $bus.get(:settings, :debug) && Gosu.button_down?(Gosu::KB_T)
|
||||||
room_coords = $bus.get(:boss_room_coords)
|
room_coords = $bus.get(:exit_room_coords)
|
||||||
if room_coords
|
if room_coords
|
||||||
@world_x, @world_y = [room_coords[0] * 60 + 5, room_coords[1] * 60 + 5]
|
@world_x, @world_y = [room_coords[0] * 120 + 90, room_coords[1] * 120 + 90]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -1,28 +1,28 @@
|
|||||||
class EnemyAI
|
class EnemyAI
|
||||||
AGGRO_RADIUS = 2000
|
|
||||||
BASE_SPEED = 130.0
|
|
||||||
CORNER_OFFSET = 35 # how far from tile center to place corner waypoint
|
CORNER_OFFSET = 35 # how far from tile center to place corner waypoint
|
||||||
ATTACK_COOLDOWN = 0.2 # seconds
|
ATTACK_COOLDOWN = 0.2 # seconds
|
||||||
|
|
||||||
def initialize(enemy, type)
|
def initialize(enemy, type)
|
||||||
@enemy = enemy
|
@enemy = enemy
|
||||||
@type = type
|
@type = type
|
||||||
|
@base_speed = case type
|
||||||
|
when :komodo then 60
|
||||||
|
when :force then 130
|
||||||
|
end
|
||||||
@tile_path = [] # raw tile coords from solver
|
@tile_path = [] # raw tile coords from solver
|
||||||
@waypoints = [] # actual world positions to move through
|
@waypoints = [] # actual world positions to move through
|
||||||
@last_player_tile = nil
|
@last_player_tile = nil
|
||||||
@avoiding_obstacle = false
|
@avoiding_obstacle = false
|
||||||
@speed = BASE_SPEED
|
@speed = @base_speed
|
||||||
@time_since_attack = ATTACK_COOLDOWN
|
@time_since_attack = ATTACK_COOLDOWN
|
||||||
end
|
end
|
||||||
|
|
||||||
def update(player_x, player_y, dt)
|
def update(player_x, player_y, dt, force = false)
|
||||||
distance = Math.sqrt((player_x - @enemy.x)**2 + (player_y - @enemy.y)**2)
|
distance = Math.hypot(@enemy.x - player_x, @enemy.y - player_y)
|
||||||
return unless distance < AGGRO_RADIUS
|
|
||||||
|
|
||||||
if @enemy.lorentz && distance < 230
|
if @enemy.lorentz && distance < 230
|
||||||
@speed = BASE_SPEED * 0.3
|
@speed = @base_speed * 0.3
|
||||||
else
|
else
|
||||||
@speed = BASE_SPEED
|
@speed = @base_speed
|
||||||
end
|
end
|
||||||
|
|
||||||
player_tile = [(player_x - 90) / 120, (player_y - 90) / 120].map(&:round)
|
player_tile = [(player_x - 90) / 120, (player_y - 90) / 120].map(&:round)
|
||||||
@@ -66,7 +66,7 @@ class EnemyAI
|
|||||||
[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]
|
closest_idx = corners.each_with_index.min_by { |c, _| Math.hypot(c[0] - @enemy.x, c[1] - @enemy.y) }[1]
|
||||||
@waypoints = corners.rotate(closest_idx)
|
@waypoints = corners.rotate(closest_idx)
|
||||||
@avoiding_obstacle = true
|
@avoiding_obstacle = true
|
||||||
end
|
end
|
||||||
@@ -76,7 +76,7 @@ class EnemyAI
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if @waypoints.empty? || @last_player_tile != player_tile
|
if force || @waypoints.empty? || @last_player_tile != player_tile
|
||||||
@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]) || []
|
@tile_path = $bus.get(:maze_solve, enemy_tile[0], enemy_tile[1], player_tile[0], player_tile[1]) || []
|
||||||
if $bus.get(:prop_at, enemy_tile[0], enemy_tile[1]).nil?
|
if $bus.get(:prop_at, enemy_tile[0], enemy_tile[1]).nil?
|
||||||
@@ -132,12 +132,12 @@ class EnemyAI
|
|||||||
]
|
]
|
||||||
|
|
||||||
# first waypoint: corner closest to where we're coming from
|
# first waypoint: corner closest to where we're coming from
|
||||||
first = corners.min_by { |cx, cy| Gosu.distance(cx, cy, prev_cx, prev_cy) }
|
first = corners.min_by { |cx, cy| Math.hypot(cx - prev_cx, cy - prev_cy) }
|
||||||
|
|
||||||
# second waypoint: closest to next tile but not opposite to first
|
# second waypoint: closest to next tile but not opposite to first
|
||||||
opposite = [center_x - (first[0] - center_x), center_y - (first[1] - center_y)]
|
opposite = [center_x - (first[0] - center_x), center_y - (first[1] - center_y)]
|
||||||
candidates = corners.reject { |c| c == first || c == opposite }
|
candidates = corners.reject { |c| c == first || c == opposite }
|
||||||
second = candidates.min_by { |cx, cy| Gosu.distance(cx, cy, next_cx, next_cy) }
|
second = candidates.min_by { |cx, cy| Math.hypot(cx - next_cx, cy - next_cy) }
|
||||||
|
|
||||||
waypoints << first
|
waypoints << first
|
||||||
waypoints << second
|
waypoints << second
|
||||||
|
|||||||
@@ -20,26 +20,50 @@ class Enemy
|
|||||||
[@i_w, @i_h, @s]
|
[@i_w, @i_h, @s]
|
||||||
end
|
end
|
||||||
|
|
||||||
def initialize(x, y, health = 100)
|
def initialize(x, y, type, health = 100)
|
||||||
@x = x
|
@x = x
|
||||||
@y = y
|
@y = y
|
||||||
@w = 25
|
@w = 25
|
||||||
@h = 30
|
@h = 30
|
||||||
|
@max_health = health
|
||||||
@health = health
|
@health = health
|
||||||
|
@type = type
|
||||||
|
@stun = 0
|
||||||
|
@last_lock = :none
|
||||||
@lorentz = $bus.get(:lorentz_field?) || false
|
@lorentz = $bus.get(:lorentz_field?) || false
|
||||||
|
|
||||||
$bus.on(:lorentz_field!) do |lorentz|
|
$bus.on(:lorentz_field!) do |lorentz|
|
||||||
@lorentz = lorentz
|
@lorentz = lorentz
|
||||||
end
|
end
|
||||||
|
|
||||||
@ai = EnemyAI.new(self, :chase)
|
@ai = EnemyAI.new(self, @type)
|
||||||
|
|
||||||
$bus.on(:collides?) do |rect|
|
$bus.on(:collides?) do |rect|
|
||||||
next collides?(rect) ? :enemy : nil
|
next collides?(rect) ? :enemy : nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
$bus.on(:blast) do |x, y, radius, damage, _|
|
||||||
|
dist = Math.hypot(@x - x, @y - y)
|
||||||
|
if dist <= radius + 20
|
||||||
|
take_damage(damage * 2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
$bus.on(:stun) do |x, y|
|
||||||
|
dist = Math.hypot(@x - x, @y - y)
|
||||||
|
if dist <= 100
|
||||||
|
@stun = 60 * 1 # stun for 1 second
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def take_damage(amount)
|
def take_damage(amount)
|
||||||
|
# knockback a bit based on player position
|
||||||
|
player_x, player_y = $bus.get(:player_position) || [0, 0]
|
||||||
|
angle = Math.atan2(@y - player_y, @x - player_x)
|
||||||
|
knockback_distance = 10
|
||||||
|
@x += Math.cos(angle) * knockback_distance
|
||||||
|
@y += Math.sin(angle) * knockback_distance
|
||||||
@health -= amount
|
@health -= amount
|
||||||
if @health <= 0
|
if @health <= 0
|
||||||
$bus.emit(:enemy_died, self)
|
$bus.emit(:enemy_died, self)
|
||||||
@@ -56,8 +80,24 @@ class Enemy
|
|||||||
end
|
end
|
||||||
|
|
||||||
def update(dt)
|
def update(dt)
|
||||||
|
if @stun > 0
|
||||||
|
@stun -= 1
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
player_pos = $bus.get(:player_position)
|
player_pos = $bus.get(:player_position)
|
||||||
@ai.update(*player_pos, dt) if player_pos
|
closest_event_horizon = $bus.get(:closest_event_horizon, @x, @y, 2000)
|
||||||
|
if closest_event_horizon
|
||||||
|
@ai.update(*closest_event_horizon, dt, @last_lock != :event_horizon)
|
||||||
|
@last_lock = :event_horizon
|
||||||
|
else
|
||||||
|
@ai.update(*player_pos, dt, @last_lock != :player) if player_pos
|
||||||
|
@last_lock = :player
|
||||||
|
end
|
||||||
|
|
||||||
|
if $bus.get_all(:collides?, rect).include?(:trap)
|
||||||
|
$bus.emit(:trap_stepped_on, rect, :enemy)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def draw
|
def draw
|
||||||
@@ -76,7 +116,8 @@ class Enemy
|
|||||||
|
|
||||||
# Health bar
|
# Health bar
|
||||||
|
|
||||||
health_width = (@w * @health / 100.0)
|
health_width = (@w * @health / @max_health)
|
||||||
|
Gosu::draw_rect(screen_x - @w / 2, screen_y - @h / 2 - 10, @w, 5, Gosu::Color.new(0xFF555555), Float::INFINITY)
|
||||||
Gosu.draw_rect(screen_x - @w / 2, screen_y - @h / 2 - 10, health_width, 5, Gosu::Color.new(0xFF00FF00), Float::INFINITY)
|
Gosu.draw_rect(screen_x - @w / 2, screen_y - @h / 2 - 10, health_width, 5, Gosu::Color.new(0xFF00FF00), Float::INFINITY)
|
||||||
|
|
||||||
if $bus.get(:settings, :debug)
|
if $bus.get(:settings, :debug)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ class Force < Enemy
|
|||||||
load 'assets/images/force.png', 21, 32, 1.7
|
load 'assets/images/force.png', 21, 32, 1.7
|
||||||
|
|
||||||
def initialize(x, y)
|
def initialize(x, y)
|
||||||
super(x, y, 100)
|
super(x, y, :force, 100)
|
||||||
|
|
||||||
$bus.on(:attack) do |attack_x, attack_y, range, damage|
|
$bus.on(:attack) do |attack_x, attack_y, range, damage|
|
||||||
# Whoa, did'nt know ruby had hypot functions built in!
|
# Whoa, did'nt know ruby had hypot functions built in!
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
require_relative 'force'
|
require_relative 'force'
|
||||||
|
require_relative 'komodo'
|
||||||
|
|
||||||
class EnemyHandler
|
class EnemyHandler
|
||||||
def initialize
|
def initialize
|
||||||
@@ -16,10 +17,11 @@ class EnemyHandler
|
|||||||
end
|
end
|
||||||
|
|
||||||
def spawn!
|
def spawn!
|
||||||
# For now, just spawn a single enemy at a fixed location
|
boss_rooms = $bus.get(:boss_rooms) || []
|
||||||
start_room_coords = $bus.get(:start_room_coords)
|
if @enemies.count < 4 && rand < (1.0 / (60 * 10))
|
||||||
if @enemies.empty? && start_room_coords
|
boss_room = boss_rooms.sample
|
||||||
@enemies << Force.new((start_room_coords[0] * 2 + 1 + 4) * 60 + 30, (start_room_coords[1] * 2 + 1 + 4) * 60 + 30)
|
type = rand < 0.1 ? Komodo : Force
|
||||||
|
@enemies << (type).new((boss_room[0] * 2 + 4) * 60 + 30, (boss_room[1] * 2 + 4) * 60 + 30)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
require_relative 'base'
|
||||||
|
|
||||||
|
class Komodo < Enemy
|
||||||
|
load 'assets/images/komodo.png', 31, 48, 1.7
|
||||||
|
|
||||||
|
def initialize(x, y)
|
||||||
|
super(x, y, :komodo, 1000)
|
||||||
|
|
||||||
|
$bus.on(:attack) do |attack_x, attack_y, range, damage|
|
||||||
|
if Math.hypot(@x - attack_x, @y - attack_y) <= range
|
||||||
|
take_damage(damage)
|
||||||
|
next true
|
||||||
|
end
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -4,12 +4,26 @@ class HealthBar
|
|||||||
def initialize(max_health)
|
def initialize(max_health)
|
||||||
@max_health = max_health
|
@max_health = max_health
|
||||||
@health = max_health
|
@health = max_health
|
||||||
|
|
||||||
|
@heart_image = Gosu::Image.new("assets/images/heart.png", retro: true)
|
||||||
|
|
||||||
|
$bus.on(:blast) do |x, y, radius, damage, safety|
|
||||||
|
next if safety == :safe
|
||||||
|
|
||||||
|
player_x, player_y = $bus.get(:player_position)
|
||||||
|
|
||||||
|
dist = Math.hypot(player_x - x, player_y - y)
|
||||||
|
if dist <= radius + 20
|
||||||
|
take_damage(damage)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def take_damage(amount)
|
def take_damage(amount)
|
||||||
@health -= amount
|
@health -= amount
|
||||||
if @health <= 0
|
if @health <= 0
|
||||||
@health = 0
|
@health = 0
|
||||||
|
$is_dead = true
|
||||||
$bus.emit(:change_scene, GameOver)
|
$bus.emit(:change_scene, GameOver)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -21,10 +35,12 @@ class HealthBar
|
|||||||
|
|
||||||
def draw
|
def draw
|
||||||
# Draw the background of the health bar (red)
|
# Draw the background of the health bar (red)
|
||||||
Gosu.draw_rect(30, 10, 100, 10, Gosu::Color::RED)
|
Gosu.draw_rect(SCREEN_SIZE[0] - 100 - 10, 180, 100, 10, Gosu::Color::RED, Float::INFINITY)
|
||||||
|
|
||||||
# Draw the foreground of the health bar (green) based on current health
|
# Draw the foreground of the health bar (green) based on current health
|
||||||
health_width = (health.to_f / max_health) * 100
|
health_width = (health.to_f / max_health) * 100
|
||||||
Gosu.draw_rect(30, 10, health_width, 10, Gosu::Color::GREEN)
|
Gosu.draw_rect(SCREEN_SIZE[0] - 100 - 10, 180, health_width, 10, Gosu::Color::GREEN, Float::INFINITY)
|
||||||
|
|
||||||
|
@heart_image.draw(SCREEN_SIZE[0] - 100 - 20, 173, Float::INFINITY, 1.5, 1.5)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -94,19 +94,19 @@ class Inventory
|
|||||||
when Gosu::KB_1 then @inventory_selected = :occams_razor
|
when Gosu::KB_1 then @inventory_selected = :occams_razor
|
||||||
when Gosu::KB_2 then @inventory_selected = :quantum_bow
|
when Gosu::KB_2 then @inventory_selected = :quantum_bow
|
||||||
when Gosu::KB_3 then @inventory_selected = :blade_of_recursion
|
when Gosu::KB_3 then @inventory_selected = :blade_of_recursion
|
||||||
when Gosu::KB_DOWN
|
when $bus.get(:settings, :"inventory down")
|
||||||
row, col = find_slot(@inventory_selected)
|
row, col = find_slot(@inventory_selected)
|
||||||
@inventory_selected = @grid[(row + 1) % @grid.size][col][0]
|
@inventory_selected = @grid[(row + 1) % @grid.size][col][0]
|
||||||
when Gosu::KB_UP
|
when $bus.get(:settings, :"inventory up")
|
||||||
row, col = find_slot(@inventory_selected)
|
row, col = find_slot(@inventory_selected)
|
||||||
@inventory_selected = @grid[(row - 1) % @grid.size][col][0]
|
@inventory_selected = @grid[(row - 1) % @grid.size][col][0]
|
||||||
when Gosu::KB_LEFT
|
when $bus.get(:settings, :"inventory left")
|
||||||
row, col = find_slot(@inventory_selected)
|
row, col = find_slot(@inventory_selected)
|
||||||
@inventory_selected = @grid[row][(col - 1) % @grid[row].size][0]
|
@inventory_selected = @grid[row][(col - 1) % @grid[row].size][0]
|
||||||
when Gosu::KB_RIGHT
|
when $bus.get(:settings, :"inventory right")
|
||||||
row, col = find_slot(@inventory_selected)
|
row, col = find_slot(@inventory_selected)
|
||||||
@inventory_selected = @grid[row][(col + 1) % @grid[row].size][0]
|
@inventory_selected = @grid[row][(col + 1) % @grid[row].size][0]
|
||||||
when Gosu::KB_RETURN
|
when $bus.get(:settings, :craft)
|
||||||
craft(@inventory_selected, ctrl: Gosu.button_down?(Gosu::KB_RIGHT_CONTROL) || Gosu.button_down?(Gosu::KB_LEFT_CONTROL))
|
craft(@inventory_selected, ctrl: Gosu.button_down?(Gosu::KB_RIGHT_CONTROL) || Gosu.button_down?(Gosu::KB_LEFT_CONTROL))
|
||||||
else
|
else
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -1,23 +1,48 @@
|
|||||||
require_relative "inventory"
|
require_relative "inventory"
|
||||||
require_relative "health"
|
require_relative "health"
|
||||||
|
require_relative "logs"
|
||||||
|
require_relative "minimap"
|
||||||
|
|
||||||
class HUDLayer
|
class HUDLayer
|
||||||
def initialize
|
def initialize
|
||||||
@hud_bg = Gosu::Image.new("assets/images/hud.png", retro: true)
|
@hud_bg = Gosu::Image.new("assets/images/hud.png", retro: true)
|
||||||
@hud_fg = Gosu::Image.new("assets/images/hud_fg.png", retro: true)
|
@hud_fg = Gosu::Image.new("assets/images/hud_fg.png", retro: true)
|
||||||
|
|
||||||
@inventory = Inventory.new
|
@inventory = Inventory.new
|
||||||
@health = HealthBar.new(100)
|
@health = HealthBar.new(100)
|
||||||
|
@logs = GameLogger.new
|
||||||
|
@minimap = Minimap.new
|
||||||
|
|
||||||
|
@direction_sprite = Gosu::Image.new("assets/images/direction.png", retro: true)
|
||||||
|
|
||||||
$bus.on(:enemy_attack) do |damage|
|
$bus.on(:enemy_attack) do |damage|
|
||||||
@health.take_damage(damage)
|
@health.take_damage(damage)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def update(dt)
|
||||||
|
@logs.update(dt)
|
||||||
|
end
|
||||||
|
|
||||||
def draw
|
def draw
|
||||||
@hud_bg.draw(0, 0, Float::INFINITY)
|
@hud_bg.draw(0, 0, Float::INFINITY)
|
||||||
|
|
||||||
@inventory.draw
|
@inventory.draw
|
||||||
@health.draw
|
@health.draw
|
||||||
|
@logs.draw
|
||||||
|
|
||||||
|
player_x, player_y = $bus.get(:player_position) || [0, 0]
|
||||||
|
tile_x = ((player_x - 90) / 120).round
|
||||||
|
tile_y = ((player_y - 90) / 120).round
|
||||||
|
center_x = tile_x * 2 + 1
|
||||||
|
center_y = tile_y * 2 + 1
|
||||||
|
@minimap.draw(center_x, center_y)
|
||||||
|
|
||||||
|
if $bus.get(:radar_triangulation?, player_x, player_y)
|
||||||
|
exit_x, exit_y = $bus.get(:exit_room_coords) || [0, 0]
|
||||||
|
angle_to_exit = Math.atan2(exit_y - tile_x, exit_x - tile_y) * 180 / Math::PI
|
||||||
|
@direction_sprite.draw_rot(SCREEN_SIZE[0] / 2, SCREEN_SIZE[1] / 2, Float::INFINITY, angle_to_exit, 0.5, 0.5, 5, 5)
|
||||||
|
end
|
||||||
|
|
||||||
@hud_fg.draw(0, 0, Float::INFINITY)
|
@hud_fg.draw(0, 0, Float::INFINITY)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
class GameLogger
|
||||||
|
MAX_ENTRIES = 5 # max messages to show
|
||||||
|
DURATION = 3.0 # seconds each message stays
|
||||||
|
|
||||||
|
Entry = Struct.new(:text, :time)
|
||||||
|
|
||||||
|
def initialize
|
||||||
|
@entries = []
|
||||||
|
@font = Gosu::Font.new(10, name: 'assets/fonts/tn.ttf')
|
||||||
|
|
||||||
|
$bus.on(:log) do |msg|
|
||||||
|
log(msg)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# add a message
|
||||||
|
def log(msg)
|
||||||
|
@entries << Entry.new(msg, DURATION)
|
||||||
|
@entries.shift if @entries.size > MAX_ENTRIES
|
||||||
|
end
|
||||||
|
|
||||||
|
def update(dt)
|
||||||
|
@entries.each { |e| e.time -= dt }
|
||||||
|
@entries.reject! { |e| e.time <= 0 }
|
||||||
|
end
|
||||||
|
|
||||||
|
def draw
|
||||||
|
padding_x = 10
|
||||||
|
padding_y = 10
|
||||||
|
line_height = 24
|
||||||
|
|
||||||
|
# bottom-left start
|
||||||
|
base_x = padding_x
|
||||||
|
base_y = SCREEN_SIZE[1] - padding_y - line_height * @entries.size
|
||||||
|
|
||||||
|
@entries.each_with_index do |e, i|
|
||||||
|
@font.draw_text(
|
||||||
|
e.text,
|
||||||
|
base_x,
|
||||||
|
base_y + i * line_height,
|
||||||
|
Float::INFINITY,
|
||||||
|
1, 1,
|
||||||
|
Gosu::Color::YELLOW
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
class Minimap
|
||||||
|
CELL_SIZE = 3 # pixels per minimap cell
|
||||||
|
RADIUS = 10 # cells around torch to update
|
||||||
|
|
||||||
|
# Please ignore my overuse of that stupid ruby conditional assignment operator
|
||||||
|
|
||||||
|
def initialize
|
||||||
|
@torches = []
|
||||||
|
@grid_cache = {}
|
||||||
|
|
||||||
|
$bus.on(:torch_placed) do |gx, gy|
|
||||||
|
add_torch(gx, gy)
|
||||||
|
end
|
||||||
|
|
||||||
|
$bus.on(:torch_removed) do |gx, gy|
|
||||||
|
remove_torch(gx, gy)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_torch(x, y)
|
||||||
|
x, y = x * 2 + 1, y * 2 + 1 # convert from room coords to grid coords
|
||||||
|
@torches << [x, y]
|
||||||
|
update_grid_area(x, y)
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove_torch(x, y)
|
||||||
|
x, y = x * 2 + 1, y * 2 + 1
|
||||||
|
@torches.delete([x, y])
|
||||||
|
update_grid_area(x, y)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Update only cells in 10-cell box around given gx,gy
|
||||||
|
def update_grid_area(cx, cy)
|
||||||
|
(cx-RADIUS..cx+RADIUS).each do |gx|
|
||||||
|
(cy-RADIUS..cy+RADIUS).each do |gy|
|
||||||
|
if @torches.any? { |tx, ty| (gx - tx).abs <= RADIUS && (gy - ty).abs <= RADIUS }
|
||||||
|
@grid_cache[[gx, gy]] = if @torches.include?([gx, gy])
|
||||||
|
:torch
|
||||||
|
elsif $bus.get(:maze_wall?, gx, gy)
|
||||||
|
:wall
|
||||||
|
else
|
||||||
|
:empty
|
||||||
|
end
|
||||||
|
else
|
||||||
|
@grid_cache.delete([gx, gy])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def draw(center_x, center_y)
|
||||||
|
half_size = 25 # 40 cells → 20 each side
|
||||||
|
map_max = $bus.get(:maze_size).map { |s| s * 2 - 1 } || 0
|
||||||
|
|
||||||
|
offset_x = SCREEN_SIZE[0] - CELL_SIZE * (half_size * 2 + 1) - 10
|
||||||
|
offset_y = 10
|
||||||
|
|
||||||
|
size_in_cells = 2 * half_size + 1
|
||||||
|
width = size_in_cells * CELL_SIZE
|
||||||
|
height = size_in_cells * CELL_SIZE
|
||||||
|
|
||||||
|
# border
|
||||||
|
Gosu.draw_rect(offset_x - 3, offset_y - 3, width + 6, height + 6, Gosu::Color.new(0xFFa9b2a2), Float::INFINITY)
|
||||||
|
|
||||||
|
(-half_size..half_size).each do |dx|
|
||||||
|
(-half_size..half_size).each do |dy|
|
||||||
|
gx = center_x + dx
|
||||||
|
gy = center_y + dy
|
||||||
|
|
||||||
|
type = if gx < 0 || gy < 0 || gx > map_max[0] || gy > map_max[1]
|
||||||
|
:wall
|
||||||
|
else
|
||||||
|
@grid_cache[[gx, gy]] || :unrevealed
|
||||||
|
end
|
||||||
|
|
||||||
|
color = case type
|
||||||
|
when :wall then Gosu::Color.new(0xFF28353e)
|
||||||
|
when :empty then Gosu::Color.new(0xFFd6dad3)
|
||||||
|
when :torch then Gosu::Color.new(0xFFfbb954)
|
||||||
|
else Gosu::Color.new(0xFF576069)
|
||||||
|
end
|
||||||
|
|
||||||
|
color = Gosu::Color::RED if gx == center_x && gy == center_y
|
||||||
|
|
||||||
|
x = (dx + half_size) * CELL_SIZE + offset_x
|
||||||
|
y = (dy + half_size) * CELL_SIZE + offset_y
|
||||||
|
|
||||||
|
Gosu.draw_rect(x, y, CELL_SIZE, CELL_SIZE, color, Float::INFINITY)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
class Upgrades
|
||||||
|
# I would do this, but I don't have time to make the artwork for it, so let's just make it a placeholder that doesn't do anything
|
||||||
|
#
|
||||||
|
end
|
||||||
@@ -49,7 +49,7 @@ class Prop
|
|||||||
end
|
end
|
||||||
|
|
||||||
def update(dt)
|
def update(dt)
|
||||||
return unless Gosu.button_down?(Gosu::MS_LEFT) || Gosu.button_down?(Gosu::KB_X)
|
return unless Gosu.button_down?(Gosu::MS_LEFT) || Gosu.button_down?($bus.get(:settings, :break))
|
||||||
|
|
||||||
player_x, player_y = $bus.get(:player_position) || [0, 0]
|
player_x, player_y = $bus.get(:player_position) || [0, 0]
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,34 @@ class PropsHandler
|
|||||||
$bus.on(:nearby_torches) do |rect|
|
$bus.on(:nearby_torches) do |rect|
|
||||||
nearby_props(rect).select { |p| p.is_a?(Torch) }.map(&:rect).map { |t| [t[0] + t[2] / 2, t[1] + t[3] / 2] }
|
nearby_props(rect).select { |p| p.is_a?(Torch) }.map(&:rect).map { |t| [t[0] + t[2] / 2, t[1] + t[3] / 2] }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
$bus.on(:radar_triangulation?) do |player_x, player_y|
|
||||||
|
radar_triangulation?(player_x, player_y)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def radar_triangulation?(player_x, player_y)
|
||||||
|
# small rect around player to find the first radar
|
||||||
|
small_rect = [player_x - 60, player_y - 60, 120, 120] # 1 block radius
|
||||||
|
first_radar = nearby_props(small_rect).find { |p| p.is_a?(Radar) }
|
||||||
|
return false unless first_radar
|
||||||
|
|
||||||
|
# larger rect to find other radars (max 15 blocks away, 15*120 pixels)
|
||||||
|
large_rect = [first_radar.x * 120 - 15 * 120, first_radar.y * 120 - 15 * 120, 30 * 120, 30 * 120]
|
||||||
|
nearby_radars = nearby_props(large_rect).select { |p| p.is_a?(Radar) }
|
||||||
|
|
||||||
|
# need at least 3 radars total
|
||||||
|
return false if nearby_radars.size < 3
|
||||||
|
|
||||||
|
nearby_radars.combination(3).any? do |r1, r2, r3|
|
||||||
|
next unless r1 == first_radar || r2 == first_radar || r3 == first_radar
|
||||||
|
|
||||||
|
d1 = Math.hypot((r1.x - r2.x), (r1.y - r2.y))
|
||||||
|
d2 = Math.hypot((r1.x - r3.x), (r1.y - r3.y))
|
||||||
|
d3 = Math.hypot((r2.x - r3.x), (r2.y - r3.y))
|
||||||
|
|
||||||
|
[d1, d2, d3].all? { |d| d >= 3 && d <= 15 }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def spawn_chests!
|
def spawn_chests!
|
||||||
@@ -49,7 +77,7 @@ class PropsHandler
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Add one in the start room for players to have something to interact with right away
|
# Add one in the start room for players to have something right away
|
||||||
start_room_x, start_room_y = $bus.get(:start_room_coords) || [0, 0]
|
start_room_x, start_room_y = $bus.get(:start_room_coords) || [0, 0]
|
||||||
x, y = [
|
x, y = [
|
||||||
[start_room_x, start_room_y],
|
[start_room_x, start_room_y],
|
||||||
@@ -94,7 +122,7 @@ class PropsHandler
|
|||||||
@grid.delete([prop.x, prop.y])
|
@grid.delete([prop.x, prop.y])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
if Gosu.button_down?(Gosu::KB_P) # place selected if it is a placeable item
|
if Gosu.button_down?($bus.get(:settings, :place))
|
||||||
selected_item = $bus.get(:selected_item)
|
selected_item = $bus.get(:selected_item)
|
||||||
if selected_item == :torch
|
if selected_item == :torch
|
||||||
player_x, player_y = $bus.get(:player_position) || [0, 0]
|
player_x, player_y = $bus.get(:player_position) || [0, 0]
|
||||||
@@ -103,6 +131,7 @@ class PropsHandler
|
|||||||
return if @grid[[tile[0], tile[1]]] # can't place if something is already there
|
return if @grid[[tile[0], tile[1]]] # can't place if something is already there
|
||||||
return if $bus.get_all(:collides?, torch.collision_rect).include?(:character) # can't place if colliding with player
|
return if $bus.get_all(:collides?, torch.collision_rect).include?(:character) # can't place if colliding with player
|
||||||
return unless $bus.get(:consume, :torch, 1)
|
return unless $bus.get(:consume, :torch, 1)
|
||||||
|
$bus.emit(:torch_placed, torch.x, torch.y)
|
||||||
add(torch)
|
add(torch)
|
||||||
elsif selected_item == :radar
|
elsif selected_item == :radar
|
||||||
player_x, player_y = $bus.get(:player_position) || [0, 0]
|
player_x, player_y = $bus.get(:player_position) || [0, 0]
|
||||||
|
|||||||
@@ -6,4 +6,13 @@ class Torch < Prop
|
|||||||
def resources
|
def resources
|
||||||
{ wood: rand(0..8) }
|
{ wood: rand(0..8) }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def update(dt)
|
||||||
|
dead = super(dt)
|
||||||
|
if dead
|
||||||
|
$bus.emit(:torch_removed, @x, @y)
|
||||||
|
pp "Emitted torch_removed for torch at #{@x}, #{@y}"
|
||||||
|
end
|
||||||
|
dead
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -3,24 +3,32 @@ require_relative 'hud/layer'
|
|||||||
require_relative 'props/handler'
|
require_relative 'props/handler'
|
||||||
require_relative 'enemy/handler'
|
require_relative 'enemy/handler'
|
||||||
require_relative 'weapons/handler'
|
require_relative 'weapons/handler'
|
||||||
|
require_relative 'traps/handler'
|
||||||
require_relative 'character'
|
require_relative 'character'
|
||||||
|
|
||||||
class Game < Scene
|
class Game < Scene
|
||||||
def initialize
|
def initialize
|
||||||
super
|
super
|
||||||
|
$is_dead = false
|
||||||
|
|
||||||
@font = Gosu::Font.new(24)
|
@font = Gosu::Font.new(24)
|
||||||
@floor_image = Gosu::Image.new("assets/images/floor.png", retro: true)
|
@floor_image = Gosu::Image.new("assets/images/floor.png", retro: true)
|
||||||
|
|
||||||
|
@blasted = 0
|
||||||
|
|
||||||
@maze = Maze.new
|
@maze = Maze.new
|
||||||
@character = Character.new
|
@character = Character.new
|
||||||
@enemies = EnemyHandler.new
|
@enemies = EnemyHandler.new
|
||||||
@props = PropsHandler.new
|
@props = PropsHandler.new
|
||||||
@weapons = WeaponHandler.new
|
@weapons = WeaponHandler.new
|
||||||
|
@traps = TrapHandler.new
|
||||||
|
|
||||||
@hud = HUDLayer.new
|
@hud = HUDLayer.new
|
||||||
|
|
||||||
@camera = [0, 0]
|
@camera = [0, 0]
|
||||||
|
|
||||||
|
$bus.emit(:log, "Welcome to the Mist!")
|
||||||
|
|
||||||
$bus.on(:player_move) do |pos|
|
$bus.on(:player_move) do |pos|
|
||||||
@camera = pos
|
@camera = pos
|
||||||
end
|
end
|
||||||
@@ -28,15 +36,26 @@ class Game < Scene
|
|||||||
$bus.on(:camera_pos) do
|
$bus.on(:camera_pos) do
|
||||||
next @camera
|
next @camera
|
||||||
end
|
end
|
||||||
|
|
||||||
|
$bus.on(:blast) do |_|
|
||||||
|
@blasted = 10 # blast for 10 frames
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def draw
|
def draw
|
||||||
|
if @blasted != 0
|
||||||
|
# this is a terrible animation for blasting but that would require me to work on artwork and I don't have time for that, so let's just flash the screen white and fade it out
|
||||||
|
Gosu.draw_rect(0, 0, *SCREEN_SIZE, Gosu::Color.new(@blasted * 255 / 10, 255, 255, 255), Float::INFINITY)
|
||||||
|
@blasted -= 1
|
||||||
|
return
|
||||||
|
end
|
||||||
draw_floor
|
draw_floor
|
||||||
@maze.draw
|
@maze.draw
|
||||||
@character.draw
|
@character.draw
|
||||||
@enemies.draw
|
@enemies.draw
|
||||||
@props.draw
|
@props.draw
|
||||||
$bus.emit(:shade) if $bus.get(:settings, :fog)
|
@traps.draw
|
||||||
|
$bus.emit(:shade)
|
||||||
@hud.draw
|
@hud.draw
|
||||||
@weapons.draw
|
@weapons.draw
|
||||||
draw_debug! if $bus.get(:settings, :debug)
|
draw_debug! if $bus.get(:settings, :debug)
|
||||||
@@ -71,15 +90,19 @@ class Game < Scene
|
|||||||
@character.update(dt)
|
@character.update(dt)
|
||||||
@enemies.update(dt)
|
@enemies.update(dt)
|
||||||
@props.update(dt)
|
@props.update(dt)
|
||||||
|
@traps.update(dt)
|
||||||
@weapons.update(dt)
|
@weapons.update(dt)
|
||||||
|
@hud.update(dt)
|
||||||
end
|
end
|
||||||
|
|
||||||
def button_down(id, pos)
|
def button_down(id, pos)
|
||||||
return $bus.emit(:change_scene, Menu.new) if id == Gosu::KB_ESCAPE
|
# Pressing ESC returns to the menu, this is bad but I don't have time to make a proper pause menu,
|
||||||
|
# as the game has no persistent state outside of the current scene, just returning to the menu resets everything
|
||||||
|
return $bus.emit(:change_scene, Menu) if id == Gosu::KB_ESCAPE
|
||||||
|
|
||||||
return if @hud.button_down(id, pos)
|
return if @hud.button_down(id, pos)
|
||||||
return if @props.button_down(id, pos)
|
return if @props.button_down(id, pos)
|
||||||
#return if @weapons.button_down(id, pos)
|
return if @weapons.button_down(id, pos)
|
||||||
|
|
||||||
#@character.button_down(id)
|
#@character.button_down(id)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
class Trap
|
||||||
|
SIZE = [40, 40].freeze
|
||||||
|
|
||||||
|
attr_reader :x, :y
|
||||||
|
|
||||||
|
def initialize(x, y)
|
||||||
|
@x = x
|
||||||
|
@y = y
|
||||||
|
@sprite = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def collides?(rect)
|
||||||
|
return intersects?(collision_rect, rect)
|
||||||
|
end
|
||||||
|
|
||||||
|
def collision_rect
|
||||||
|
[@x - SIZE[0] / 2, @y - SIZE[1] / 2, *SIZE]
|
||||||
|
end
|
||||||
|
|
||||||
|
def update(player)
|
||||||
|
# the collision is checked by the player or enemy
|
||||||
|
end
|
||||||
|
|
||||||
|
def draw
|
||||||
|
return unless @sprite
|
||||||
|
|
||||||
|
cam_x, cam_y = $bus.get(:camera_pos) || [0, 0]
|
||||||
|
@sprite.draw(@x - SIZE[0] / 2 - cam_x, @y - SIZE[1] / 2 - cam_y, 0, 2, 2)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
require_relative 'base'
|
||||||
|
|
||||||
|
class EventHorizon < Trap
|
||||||
|
def initialize(x, y)
|
||||||
|
super(x, y)
|
||||||
|
@sprite = Gosu::Image.new("assets/images/event_horizon.png", retro: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
def stepped_on(entity)
|
||||||
|
# this is not a weapon, so nothing
|
||||||
|
# it is instead destroyed after after being stepped on by enemy
|
||||||
|
$bus.emit(:stun, @x, @y)
|
||||||
|
entity == :enemy
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
require_relative 'landmine'
|
||||||
|
require_relative 'shrodingers_mine'
|
||||||
|
require_relative 'event_horizon'
|
||||||
|
|
||||||
|
class TrapHandler
|
||||||
|
def initialize
|
||||||
|
# traps are stored in a grid for efficient collision checking
|
||||||
|
# can write in report about trying to use an array and how it was too slow to check every trap for collisions, so we switched to a grid where each cell is 120x120, blah blah
|
||||||
|
@grid = {}
|
||||||
|
|
||||||
|
$bus.on(:collides?) do |rect|
|
||||||
|
nearby_traps(rect).any? { |t| t.collides?(rect) } ? :trap : nil
|
||||||
|
end
|
||||||
|
|
||||||
|
$bus.on(:trap_stepped_on) do |rect, entity|
|
||||||
|
nearby_traps(rect).each do |t|
|
||||||
|
t.collides?(rect) && t.stepped_on(entity) && @grid[[(t.x / 120).floor, (t.y / 120).floor]].delete(t) # remove the trap if stepped on
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
$bus.on(:closest_event_horizon) do |x, y, range|
|
||||||
|
# this abomination is to find the closest event horizon within range, we can optimize this later if needed but for now it works
|
||||||
|
nearby_traps([x - range, y - range, range * 2, range * 2])
|
||||||
|
.select { |t| t.is_a?(EventHorizon) }
|
||||||
|
.map { |t| [t, Math.hypot(t.x - x, t.y - y)] }
|
||||||
|
.select { |_, d| d <= range }
|
||||||
|
.min_by { |_, d| d }
|
||||||
|
&.first&.then { |e| [e.x, e.y] }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def add(trap)
|
||||||
|
tx = (trap.x / 120).floor
|
||||||
|
ty = (trap.y / 120).floor
|
||||||
|
|
||||||
|
@grid[[tx, ty]] ||= []
|
||||||
|
@grid[[tx, ty]] << trap
|
||||||
|
end
|
||||||
|
|
||||||
|
def update dt
|
||||||
|
# allow placing traps by the player
|
||||||
|
if Gosu.button_down?(Gosu::MS_LEFT)
|
||||||
|
selected_item = $bus.get(:selected_item)
|
||||||
|
if selected_item == :landmine
|
||||||
|
mouse_x, mouse_y = $bus.get(:mouse_pos) || [0, 0]
|
||||||
|
player_x, player_y = $bus.get(:player_position) || [0, 0]
|
||||||
|
cam_x, cam_y = $bus.get(:camera_pos) || [0, 0]
|
||||||
|
world_x = mouse_x + cam_x
|
||||||
|
world_y = mouse_y + cam_y
|
||||||
|
landmine = Landmine.new(world_x, world_y)
|
||||||
|
return if Math.hypot(landmine.x - player_x, landmine.y - player_y) > 200 # can't place if too far from player
|
||||||
|
return if ($bus.get_all(:collides?, landmine.collision_rect) & [:character, :prop, :trap, :wall]).any?
|
||||||
|
return unless $bus.get(:consume, :landmine, 1)
|
||||||
|
add(landmine)
|
||||||
|
elsif selected_item == :shrodingers_mine
|
||||||
|
mouse_x, mouse_y = $bus.get(:mouse_pos) || [0, 0]
|
||||||
|
player_x, player_y = $bus.get(:player_position) || [0, 0]
|
||||||
|
cam_x, cam_y = $bus.get(:camera_pos) || [0, 0]
|
||||||
|
world_x = mouse_x + cam_x
|
||||||
|
world_y = mouse_y + cam_y
|
||||||
|
mine = ShrodingersMine.new(world_x, world_y)
|
||||||
|
return if Math.hypot(mine.x - player_x, mine.y - player_y) > 200
|
||||||
|
return if ($bus.get_all(:collides?, mine.collision_rect) & [:character, :prop, :trap, :wall]).any?
|
||||||
|
return unless $bus.get(:consume, :shrodingers_mine, 1)
|
||||||
|
add(mine)
|
||||||
|
elsif selected_item == :event_horizon
|
||||||
|
mouse_x, mouse_y = $bus.get(:mouse_pos) || [0, 0]
|
||||||
|
player_x, player_y = $bus.get(:player_position) || [0, 0]
|
||||||
|
cam_x, cam_y = $bus.get(:camera_pos) || [0, 0]
|
||||||
|
world_x = mouse_x + cam_x
|
||||||
|
world_y = mouse_y + cam_y
|
||||||
|
event_horizon = EventHorizon.new(world_x, world_y)
|
||||||
|
return if Math.hypot(world_x - player_x, world_y - player_y) > 200
|
||||||
|
return if ($bus.get_all(:collides?, event_horizon.collision_rect) & [:character, :prop, :trap, :wall]).any?
|
||||||
|
return unless $bus.get(:consume, :event_horizon, 1)
|
||||||
|
add(event_horizon)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def nearby_traps(rect)
|
||||||
|
x, y, w, h = rect
|
||||||
|
|
||||||
|
min_tx = ((x) / 120).floor
|
||||||
|
max_tx = ((x + w) / 120).floor
|
||||||
|
min_ty = ((y) / 120).floor
|
||||||
|
max_ty = ((y + h) / 120).floor
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
(min_tx..max_tx).each do |tx|
|
||||||
|
(min_ty..max_ty).each do |ty|
|
||||||
|
t = @grid[[tx, ty]]
|
||||||
|
results.concat(t) if t
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
results
|
||||||
|
end
|
||||||
|
|
||||||
|
def draw
|
||||||
|
cam = $bus.get(:camera_pos) || [0, 0]
|
||||||
|
nearby_traps([*cam, *SCREEN_SIZE]).each(&:draw)
|
||||||
|
draw_ghost_traps!
|
||||||
|
end
|
||||||
|
|
||||||
|
def draw_ghost_traps!
|
||||||
|
selected_item = $bus.get(:selected_item)
|
||||||
|
return unless [:landmine, :shrodingers_mine, :event_horizon].include?(selected_item) # only draw if selected item is a placeable trap
|
||||||
|
return unless $bus.get(:count, selected_item) > 0 # only draw if player has at least 1 landmine
|
||||||
|
|
||||||
|
mouse_x, mouse_y = $bus.get(:mouse_pos) || [0, 0]
|
||||||
|
cam_x, cam_y = $bus.get(:camera_pos) || [0, 0]
|
||||||
|
world_x = mouse_x + cam_x
|
||||||
|
world_y = mouse_y + cam_y
|
||||||
|
|
||||||
|
color = Gosu::Color.new(128, 255, 255, 255)
|
||||||
|
player_x, player_y = $bus.get(:player_position) || [0, 0]
|
||||||
|
if Math.hypot(world_x - player_x, world_y - player_y) > 100 ||
|
||||||
|
($bus.get_all(:collides?, [world_x - 20, world_y - 20, 40, 40]) & [:character, :prop, :trap, :wall]).any?
|
||||||
|
color = Gosu::Color.new(128, 255, 0, 0) # red if too far
|
||||||
|
elsif nearby_traps([world_x - 20, world_y - 20, 40, 40]).any? { |t| t.collides?([world_x - 20, world_y - 20, 40, 40]) }
|
||||||
|
color = Gosu::Color.new(128, 255, 255, 0) # yellow if colliding with another trap
|
||||||
|
end
|
||||||
|
|
||||||
|
ghost_sprite = Gosu::Image.new("assets/images/#{selected_item}.png", retro: true)
|
||||||
|
ghost_sprite.draw(world_x - 20 - cam_x, world_y - 20 - cam_y, 0, 2, 2, color)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
require_relative 'base'
|
||||||
|
|
||||||
|
class Landmine < Trap
|
||||||
|
def initialize(x, y)
|
||||||
|
super(x, y)
|
||||||
|
@sprite = Gosu::Image.new("assets/images/landmine.png", retro: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
def stepped_on(_)
|
||||||
|
$bus.emit(:blast, @x, @y, 30, 30, :unsafe) # blast radius of 30 and damage of 30, and mark it as unsafe for the player
|
||||||
|
true # to destroy the trap
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
require_relative 'base'
|
||||||
|
|
||||||
|
class ShrodingersMine < Trap
|
||||||
|
def initialize(x, y)
|
||||||
|
super(x, y)
|
||||||
|
@sprite = Gosu::Image.new("assets/images/shrodingers_mine.png", retro: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
def stepped_on(entity)
|
||||||
|
return false if entity == :character # shrodingers mine is safe for the player, so don't destroy it or cause a blast if the player steps on it
|
||||||
|
if rand < 0.7 # 70% chance to be a landmine, 30% chance to be a dud
|
||||||
|
$bus.emit(:blast, @x, @y, 50, 50, :safe) # blast radius of 50 and damage of 50, and always safe for the player
|
||||||
|
else
|
||||||
|
$bus.emit(:log, "You hear a click, but nothing happens. It was a dud!")
|
||||||
|
end
|
||||||
|
true # to destroy the trap
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,16 +1,25 @@
|
|||||||
class BladeOfRecursion
|
class BladeOfRecursion
|
||||||
def initialize
|
def initialize
|
||||||
@damage = 1
|
@damage = 1
|
||||||
@range = 200
|
@range = 150
|
||||||
@speed = 500
|
@speed = 400
|
||||||
@x = 0
|
@x = 0
|
||||||
@y = 0
|
@y = 0
|
||||||
|
@start_x = 0
|
||||||
|
@start_y = 0
|
||||||
@direction = nil
|
@direction = nil
|
||||||
|
|
||||||
|
# the knife is pointed top-right
|
||||||
|
@sprite = Gosu::Image.new('assets/images/blade_of_recursion.png', retro: true)
|
||||||
|
|
||||||
|
@memory_sprite = Gosu::Image.new('assets/images/memory.png', retro: true)
|
||||||
end
|
end
|
||||||
|
|
||||||
def attack(direction)
|
def attack(direction)
|
||||||
|
return if @direction
|
||||||
@direction = direction
|
@direction = direction
|
||||||
@x, @y = $bus.get(:player_position)
|
@x, @y = $bus.get(:player_position)
|
||||||
|
@start_x, @start_y = @x, @y
|
||||||
end
|
end
|
||||||
|
|
||||||
def update(dt)
|
def update(dt)
|
||||||
@@ -19,12 +28,64 @@ class BladeOfRecursion
|
|||||||
@x += Math.cos(@direction) * @speed * dt
|
@x += Math.cos(@direction) * @speed * dt
|
||||||
@y += Math.sin(@direction) * @speed * dt
|
@y += Math.sin(@direction) * @speed * dt
|
||||||
|
|
||||||
$bus.get(:attack, @x, @y, @range, @damage)
|
if Math.hypot(@x - @start_x, @y - @start_y) > @range
|
||||||
|
|
||||||
# If the blade has traveled its max range, reset it
|
|
||||||
player_x, player_y = $bus.get(:player_position)
|
|
||||||
if Math.hypot(@x - player_x, @y - player_y) > @range
|
|
||||||
@direction = nil
|
@direction = nil
|
||||||
|
@damage = 1
|
||||||
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
collides = $bus.get_all(:collides?, [@x - 5, @y - 5, 10, 10])
|
||||||
|
if collides.include?(:enemy)
|
||||||
|
$bus.emit(:attack, @x, @y, 30, @damage)
|
||||||
|
@direction = nil
|
||||||
|
@damage *= 2
|
||||||
|
if @damage > 512
|
||||||
|
damage = 1
|
||||||
|
$bus.emit(:consume, :blade_of_recursion, 1)
|
||||||
|
end
|
||||||
|
elsif collides.include?(:wall)
|
||||||
|
@direction = nil
|
||||||
|
@damage = 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def draw
|
||||||
|
cam_x, cam_y = $bus.get(:camera_pos) || [0, 0]
|
||||||
|
|
||||||
|
# Draw the blade if active
|
||||||
|
if @direction
|
||||||
|
# Convert radians to degrees
|
||||||
|
angle_deg = @direction * 180 / Math::PI
|
||||||
|
# Adjust for knife pointing top-right
|
||||||
|
angle_deg += 45
|
||||||
|
|
||||||
|
@sprite.draw_rot(
|
||||||
|
@x - cam_x, # x in world coords
|
||||||
|
@y - cam_y, # y in world coords
|
||||||
|
1, # z-order
|
||||||
|
angle_deg, # rotation
|
||||||
|
0.5, 0.5, # pivot center
|
||||||
|
2, 2,
|
||||||
|
Gosu::Color::WHITE
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Draw memory bar as HUD in top-right of screen
|
||||||
|
# this should be in the HUD layer but due to time constraints im putting it here, sorry
|
||||||
|
bar_width = 100
|
||||||
|
bar_height = 10
|
||||||
|
|
||||||
|
bar_x = SCREEN_SIZE[0] - bar_width - 10
|
||||||
|
bar_y = 205
|
||||||
|
|
||||||
|
# full red background
|
||||||
|
Gosu.draw_rect(bar_x, bar_y, bar_width, bar_height, Gosu::Color::RED, Float::INFINITY)
|
||||||
|
|
||||||
|
# green foreground representing current damage
|
||||||
|
damage_ratio = Math.log2(@damage) / 9.0
|
||||||
|
damage_width = bar_width * [damage_ratio, 1.0].min
|
||||||
|
Gosu.draw_rect(bar_x, bar_y, damage_width, bar_height, Gosu::Color::GREEN, Float::INFINITY)
|
||||||
|
|
||||||
|
@memory_sprite.draw(bar_x - 10, bar_y - 7, Float::INFINITY, 1.5, 1.5)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -1,39 +1,45 @@
|
|||||||
require_relative 'lorentz_field'
|
require_relative 'lorentz_field'
|
||||||
require_relative 'occams_razor'
|
require_relative 'occams_razor'
|
||||||
#require_relative 'quantum_bow'
|
require_relative 'quantum_bow'
|
||||||
#require_relative 'blade_of_recursion'
|
require_relative 'blade_of_recursion'
|
||||||
|
|
||||||
class WeaponHandler
|
class WeaponHandler
|
||||||
def initialize
|
def initialize
|
||||||
@weapons = {
|
@weapons = {
|
||||||
lorentz_field: LorentzField.new,
|
lorentz_field: LorentzField.new,
|
||||||
occams_razor: OccamsRazor.new,
|
occams_razor: OccamsRazor.new,
|
||||||
#quantum_bow: QuantumBow.new,
|
quantum_bow: QuantumBow.new,
|
||||||
#blade_of_recursion: BladeOfRecursion.new
|
blade_of_recursion: BladeOfRecursion.new
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def update(dt)
|
def update(dt)
|
||||||
@weapons.each_value { |weapon| weapon.update(dt) }
|
@weapons.each_value { |weapon| weapon.update(dt) }
|
||||||
|
|
||||||
selected_item = $bus.get(:selected_item)
|
|
||||||
return unless @weapons.key?(selected_item)
|
|
||||||
|
|
||||||
weapon = @weapons[selected_item]
|
|
||||||
if $bus.get(:count, selected_item) > 0
|
|
||||||
if Gosu.button_down?(Gosu::KB_SPACE)
|
|
||||||
direction = $bus.get(:player_direction)
|
|
||||||
weapon.attack(direction)
|
|
||||||
elsif Gosu.button_down?(Gosu::MS_LEFT)
|
|
||||||
player_x, player_y = $bus.get(:player_position)
|
|
||||||
mouse_x, mouse_y = $bus.get(:mouse_position)
|
|
||||||
direction = Math.atan2(mouse_y - player_y, mouse_x - player_x)
|
|
||||||
weapon.attack(direction)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def draw
|
def draw
|
||||||
@weapons.each_value(&:draw)
|
@weapons.each_value(&:draw)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def button_down(id, pos)
|
||||||
|
selected_item = $bus.get(:selected_item)
|
||||||
|
return unless @weapons.key?(selected_item)
|
||||||
|
|
||||||
|
weapon = @weapons[selected_item]
|
||||||
|
if $bus.get(:count, selected_item) > 0
|
||||||
|
if id == $bus.get(:settings, :attack)
|
||||||
|
player_x, player_y = $bus.get(:player_position)
|
||||||
|
cam_x, cam_y = $bus.get(:camera_pos) || [0, 0]
|
||||||
|
mouse_x, mouse_y = $bus.get(:mouse_pos) || [0, 0]
|
||||||
|
direction = Math.atan2((mouse_y + cam_y - player_y), (mouse_x + cam_x - player_x))
|
||||||
|
weapon.attack(direction)
|
||||||
|
elsif id == Gosu::MS_LEFT
|
||||||
|
player_x, player_y = $bus.get(:player_position)
|
||||||
|
cam_x, cam_y = $bus.get(:camera_pos) || [0, 0]
|
||||||
|
mouse_x, mouse_y = pos
|
||||||
|
direction = Math.atan2((mouse_y + cam_y - player_y), (mouse_x + cam_x - player_x))
|
||||||
|
weapon.attack(direction)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -1,17 +1,16 @@
|
|||||||
class LorentzField
|
class LorentzField
|
||||||
BASE_TIME = 25 # Base time for the field to be active in seconds
|
BASE_TIME = 25 # Base time for the field to be active in seconds
|
||||||
|
|
||||||
def initialize(bus)
|
def initialize
|
||||||
@bus = bus
|
|
||||||
@time = 0
|
@time = 0
|
||||||
@active = false
|
@active = false
|
||||||
@font = Gosu::Font.new(24, name: "assets/fonts/tn.ttf")
|
@font = Gosu::Font.new(24, name: "assets/fonts/tn.ttf")
|
||||||
|
|
||||||
@bus.on(:lorentz_field?) do
|
$bus.on(:lorentz_field?) do
|
||||||
next @active
|
next @active
|
||||||
end
|
end
|
||||||
|
|
||||||
@bus.on(:lorentz_field) do
|
$bus.on(:lorentz_field) do
|
||||||
if @active
|
if @active
|
||||||
t = @time
|
t = @time
|
||||||
remaining = BASE_TIME - t
|
remaining = BASE_TIME - t
|
||||||
@@ -34,10 +33,10 @@ class LorentzField
|
|||||||
|
|
||||||
def attack(_direction)
|
def attack(_direction)
|
||||||
return if @active
|
return if @active
|
||||||
@bus.emit(:consume, :lorentz_field, 1)
|
$bus.emit(:consume, :lorentz_field, 1)
|
||||||
@active = true
|
@active = true
|
||||||
@time = 0
|
@time = 0
|
||||||
@bus.emit(:lorentz_field!, true)
|
$bus.emit(:lorentz_field!, true)
|
||||||
end
|
end
|
||||||
|
|
||||||
def update(dt)
|
def update(dt)
|
||||||
@@ -47,7 +46,7 @@ class LorentzField
|
|||||||
if @time >= BASE_TIME
|
if @time >= BASE_TIME
|
||||||
@time = 0
|
@time = 0
|
||||||
@active = false
|
@active = false
|
||||||
@bus.emit(:lorentz_field!, false)
|
$bus.emit(:lorentz_field!, false)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -8,12 +8,15 @@ class OccamsRazor
|
|||||||
@range = 45 # how many pixels from player center to enemy center the attack hits, this is a melee weapon so it should be small
|
@range = 45 # how many pixels from player center to enemy center the attack hits, this is a melee weapon so it should be small
|
||||||
end
|
end
|
||||||
|
|
||||||
def attack(_direction)
|
def attack(direction)
|
||||||
return if @active
|
return if @active
|
||||||
|
|
||||||
@active = true
|
@active = true
|
||||||
@time = BASE_TIME
|
@time = BASE_TIME
|
||||||
|
|
||||||
|
@direction = direction # radians
|
||||||
|
@spread = Math::PI # 90° total arc
|
||||||
|
|
||||||
# Check for enemies in range and deal damage
|
# Check for enemies in range and deal damage
|
||||||
player_x, player_y = $bus.get(:player_position)
|
player_x, player_y = $bus.get(:player_position)
|
||||||
$bus.emit(:attack, player_x, player_y, @range, @damage)
|
$bus.emit(:attack, player_x, player_y, @range, @damage)
|
||||||
@@ -23,6 +26,10 @@ class OccamsRazor
|
|||||||
return unless @active
|
return unless @active
|
||||||
|
|
||||||
@time -= dt
|
@time -= dt
|
||||||
|
|
||||||
|
progress = @time / BASE_TIME
|
||||||
|
@spread = (Math::PI) * progress
|
||||||
|
|
||||||
if @time <= 0
|
if @time <= 0
|
||||||
@active = false
|
@active = false
|
||||||
@time = 0
|
@time = 0
|
||||||
@@ -30,6 +37,27 @@ class OccamsRazor
|
|||||||
end
|
end
|
||||||
|
|
||||||
def draw
|
def draw
|
||||||
# Nothing for now, but player attack animation would go here
|
return unless @active
|
||||||
|
|
||||||
|
px, py = $bus.get(:player_position)
|
||||||
|
cam_x, cam_y = $bus.get(:camera_pos) || [0, 0]
|
||||||
|
|
||||||
|
cx = px - cam_x
|
||||||
|
cy = py - cam_y
|
||||||
|
|
||||||
|
step = 0.1 # radians (~6°)
|
||||||
|
|
||||||
|
angle_start = @direction - @spread
|
||||||
|
angle_end = @direction + @spread
|
||||||
|
|
||||||
|
alpha = ((@time / BASE_TIME) * 255).to_i
|
||||||
|
color = Gosu::Color.new(alpha, 255, 255, 255)
|
||||||
|
|
||||||
|
(angle_start..angle_end).step(step) do |rad|
|
||||||
|
x = cx + Math.cos(rad) * @range
|
||||||
|
y = cy + Math.sin(rad) * @range
|
||||||
|
|
||||||
|
Gosu.draw_rect(x, y, 4, 4, color, Float::INFINITY)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
class QuantumBow
|
||||||
|
ARROW_SPEED = 700
|
||||||
|
LOW_DAMAGE = 10
|
||||||
|
HIGH_DAMAGE = 50
|
||||||
|
FACING_THRESHOLD = Math::PI / 2.0 # 90° in radians
|
||||||
|
|
||||||
|
def initialize
|
||||||
|
@arrows = []
|
||||||
|
#@sprite = Gosu::Image.new('assets/images/quantum_bow.png', retro: true)
|
||||||
|
@arrow_sprite = Gosu::Image.new('assets/images/arrow.png', retro: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
def clamp_angle(intended, player)
|
||||||
|
# it takes the direction from the attack function and only allows attacks facing forwards from the player,
|
||||||
|
# and if it is not in 90 deg from players direction then it will return the extreme angle possible else the intended angle
|
||||||
|
angle_diff = ((intended - player + Math::PI) % (2 * Math::PI)) - Math::PI
|
||||||
|
if angle_diff > FACING_THRESHOLD
|
||||||
|
return player + FACING_THRESHOLD
|
||||||
|
elsif angle_diff < -FACING_THRESHOLD
|
||||||
|
return player - FACING_THRESHOLD
|
||||||
|
else
|
||||||
|
return intended
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def attack(direction)
|
||||||
|
player_x, player_y = $bus.get(:player_position)
|
||||||
|
return unless $bus.get(:consume, :wood, 3) && $bus.get(:consume, :metal, 1)
|
||||||
|
|
||||||
|
@arrows << { x: player_x, y: player_y, direction: clamp_angle(direction, $bus.get(:player_direction)) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def update(dt)
|
||||||
|
player_x, player_y = $bus.get(:player_position)
|
||||||
|
player_dir = $bus.get(:player_direction)
|
||||||
|
|
||||||
|
@arrows.each do |arrow|
|
||||||
|
arrow[:x] += Math.cos(arrow[:direction]) * ARROW_SPEED * dt
|
||||||
|
arrow[:y] += Math.sin(arrow[:direction]) * ARROW_SPEED * dt
|
||||||
|
|
||||||
|
rect = [arrow[:x] - 5, arrow[:y] - 2, 10, 4]
|
||||||
|
collides = $bus.get_all(:collides?, rect)
|
||||||
|
|
||||||
|
if collides.include?(:enemy)
|
||||||
|
# compute if player is facing the arrow
|
||||||
|
vec_x = arrow[:x] - player_x
|
||||||
|
vec_y = arrow[:y] - player_y
|
||||||
|
angle_to_arrow = Math.atan2(vec_y, vec_x)
|
||||||
|
angle_diff = ((angle_to_arrow - player_dir + Math::PI) % (2 * Math::PI)) - Math::PI
|
||||||
|
angle_diff = angle_diff.abs
|
||||||
|
|
||||||
|
if angle_diff > FACING_THRESHOLD
|
||||||
|
$bus.emit(:blast, arrow[:x], arrow[:y], 30, HIGH_DAMAGE, :safe)
|
||||||
|
else
|
||||||
|
$bus.emit(:attack, arrow[:x], arrow[:y], 30, LOW_DAMAGE)
|
||||||
|
end
|
||||||
|
|
||||||
|
arrow[:hit] = true
|
||||||
|
elsif collides.include?(:wall)
|
||||||
|
arrow[:hit] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@arrows.reject! { |a| a[:hit] }
|
||||||
|
end
|
||||||
|
|
||||||
|
def draw
|
||||||
|
cam_x, cam_y = $bus.get(:camera_pos) || [0, 0]
|
||||||
|
|
||||||
|
@arrows.each do |arrow|
|
||||||
|
angle_deg = arrow[:direction] * 180 / Math::PI
|
||||||
|
angle_deg -= 45 + 180
|
||||||
|
@arrow_sprite.draw_rot(arrow[:x] - cam_x, arrow[:y] - cam_y, 1, angle_deg, 0.5, 0.5, 2, 2, Gosu::Color::WHITE)
|
||||||
|
end
|
||||||
|
|
||||||
|
#player_x, player_y = $bus.get(:player_position)
|
||||||
|
#@sprite.draw(player_x - cam_x - 16, player_y - cam_y - 16, 1)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -8,7 +8,7 @@ class GameOver < Scene
|
|||||||
super
|
super
|
||||||
@font = Gosu::Font.new(32, name: "assets/fonts/tn.ttf")
|
@font = Gosu::Font.new(32, name: "assets/fonts/tn.ttf")
|
||||||
@selected_index = 0
|
@selected_index = 0
|
||||||
@bg_image = Gosu::Image.new("assets/images/game_over_bg.png", retro: true)
|
@bg_image = Gosu::Image.new("assets/images/game_over_#{$is_dead ? 'dead' : 'victory'}_bg.png", retro: true)
|
||||||
@last_mouse_pos = nil
|
@last_mouse_pos = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ class GameOver < Scene
|
|||||||
@bg_image.draw(0, 0, 1)
|
@bg_image.draw(0, 0, 1)
|
||||||
|
|
||||||
OPTIONS.each_with_index do |(text, _), index|
|
OPTIONS.each_with_index do |(text, _), index|
|
||||||
y = 260 + index * 50
|
y = 360 + index * 50
|
||||||
if index == @selected_index
|
if index == @selected_index
|
||||||
@font.draw_text("[ #{text.ljust(10)} ]", 50, y, 1, 1, 1, Gosu::Color::YELLOW)
|
@font.draw_text("[ #{text.ljust(10)} ]", 50, y, 1, 1, 1, Gosu::Color::YELLOW)
|
||||||
else
|
else
|
||||||
@@ -56,7 +56,7 @@ class GameOver < Scene
|
|||||||
|
|
||||||
def item_at(pos)
|
def item_at(pos)
|
||||||
OPTIONS.each_with_index do |(_, action), index|
|
OPTIONS.each_with_index do |(_, action), index|
|
||||||
y = 260 + index * 50
|
y = 360 + index * 50
|
||||||
if pos[1].between?(y, y + 32) && pos[0].between?(50, 50 + @font.text_width(" " * 14))
|
if pos[1].between?(y, y + 32) && pos[0].between?(50, 50 + @font.text_width(" " * 14))
|
||||||
return [index, action]
|
return [index, action]
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ class Menu < Scene
|
|||||||
OPTIONS = {
|
OPTIONS = {
|
||||||
"Start" => -> { $bus.emit(:change_scene, Game) },
|
"Start" => -> { $bus.emit(:change_scene, Game) },
|
||||||
"Settings" => -> { $bus.emit(:change_scene, Settings) },
|
"Settings" => -> { $bus.emit(:change_scene, Settings) },
|
||||||
|
"Credits" => -> { $bus.emit(:change_scene, Credits) },
|
||||||
"Quit" => -> { $bus.emit(:quit_game) }
|
"Quit" => -> { $bus.emit(:quit_game) }
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
|
|||||||
@@ -5,28 +5,55 @@ N, S, W, E = 1, 2, 4, 8
|
|||||||
|
|
||||||
# Config module to hold global settings that can be toggled in the settings scene, and accessed anywhere else in the code via the event bus
|
# Config module to hold global settings that can be toggled in the settings scene, and accessed anywhere else in the code via the event bus
|
||||||
module Config
|
module Config
|
||||||
@config = {
|
@debug = false
|
||||||
debug: ARGV.include?("--debug"),
|
|
||||||
fog: true,
|
@keybinds = {
|
||||||
torches_lightup: true,
|
forward: Gosu::KB_W,
|
||||||
|
backward: Gosu::KB_S,
|
||||||
|
leftward: Gosu::KB_A,
|
||||||
|
rightward: Gosu::KB_D,
|
||||||
|
break: Gosu::KB_X,
|
||||||
|
attack: Gosu::KB_SPACE,
|
||||||
|
place: Gosu::KB_P,
|
||||||
|
"inventory up": Gosu::KB_UP,
|
||||||
|
"inventory down": Gosu::KB_DOWN,
|
||||||
|
"inventory right": Gosu::KB_RIGHT,
|
||||||
|
"inventory left": Gosu::KB_LEFT,
|
||||||
|
craft: Gosu::KB_RETURN,
|
||||||
}
|
}
|
||||||
|
|
||||||
if File.exist?("config.json")
|
if File.exist?("config.json")
|
||||||
data = JSON.parse(File.read("config.json"))
|
data = JSON.parse(File.read("config.json"))
|
||||||
@config.merge!(data.transform_keys(&:to_sym))
|
@debug = data["debug"] || false
|
||||||
|
@keybinds.merge!(data["keybinds"]&.transform_keys(&:to_sym) || {})
|
||||||
end
|
end
|
||||||
|
|
||||||
$bus.on(:settings, :master) { |k| @config[k] }
|
$bus.on(:settings, :master) { |k| k == :debug ? @debug : @keybinds[k] }
|
||||||
|
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
def toggle(key)
|
def bindings
|
||||||
@config[key] = !@config[key]
|
@keybinds
|
||||||
|
end
|
||||||
|
|
||||||
|
def debug
|
||||||
|
@debug
|
||||||
|
end
|
||||||
|
|
||||||
|
def debug=(value)
|
||||||
|
@debug = value
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_key(action, key)
|
||||||
|
@keybinds[action] = key
|
||||||
end
|
end
|
||||||
|
|
||||||
def persist!
|
def persist!
|
||||||
File.open("config.json", "w") do |f|
|
File.open("config.json", "w") do |f|
|
||||||
f.write(JSON.pretty_generate(@config))
|
f.write(JSON.pretty_generate({
|
||||||
|
keybinds: @keybinds,
|
||||||
|
debug: @debug
|
||||||
|
}))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -1,15 +1,76 @@
|
|||||||
class Settings < Scene
|
class Settings < Scene
|
||||||
def initialize
|
def initialize
|
||||||
super
|
super
|
||||||
@font = Gosu::Font.new(24)
|
@font_title = Gosu::Font.new(28, name: "assets/fonts/tn.ttf")
|
||||||
|
@font = Gosu::Font.new(20, name: "assets/fonts/tn.ttf")
|
||||||
|
@selected = 0
|
||||||
|
@waiting_for_key = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
def draw
|
def draw
|
||||||
Gosu.draw_rect(0, 0, SCREEN_SIZE[0], SCREEN_SIZE[1], Gosu::Color::BLACK)
|
Gosu.draw_rect(0, 0, *SCREEN_SIZE, Gosu::Color.new(0xFF131313))
|
||||||
@font.draw_text("Settings Scene - Press ESC to return to Menu", 50, 50, 1, 1, 1, Gosu::Color::WHITE)
|
|
||||||
|
title = "Settings"
|
||||||
|
@font_title.draw_text(title, center_x(title), 40, 1, 1, 1, Gosu::Color::YELLOW)
|
||||||
|
|
||||||
|
debug_option = "Debug Mode".ljust(15) + " : " + "#{Config.debug ? 'ON' : 'OFF'}".rjust(12)
|
||||||
|
debug_option = "> #{debug_option} <" if @selected == 0
|
||||||
|
@font.draw_text(debug_option, center_x(debug_option), 90, 1, 1, 1, @selected == 0 ? Gosu::Color::YELLOW : Gosu::Color::WHITE)
|
||||||
|
|
||||||
|
Config.bindings.each_with_index do |opt, i|
|
||||||
|
y = 120 + i * 25
|
||||||
|
selected = (i == @selected - 1)
|
||||||
|
|
||||||
|
text = "#{opt[0].to_s.ljust(15)} : #{Gosu.button_name(opt[1]).rjust(12)}"
|
||||||
|
text = "> #{text} <" if selected
|
||||||
|
|
||||||
|
color = selected ? Gosu::Color::YELLOW : Gosu::Color::WHITE
|
||||||
|
@font.draw_text(text, center_x(text), y, 1, 1, 1, color)
|
||||||
|
end
|
||||||
|
|
||||||
|
if @waiting_for_key
|
||||||
|
msg = "Press a key... (ESC to cancel)"
|
||||||
|
@font.draw_text(msg, center_x(msg), 430, 1, 1, 1, Gosu::Color::GRAY)
|
||||||
|
else
|
||||||
|
hint = "Enter: Select | ESC: Back"
|
||||||
|
@font.draw_text(hint, center_x(hint), 430, 1, 1, 1, Gosu::Color::GRAY)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def button_down(id, _pos)
|
def button_down(id, _pos)
|
||||||
$bus.emit(:change_scene, Menu.new) if id == Gosu::KB_ESCAPE
|
# waiting for keybind input
|
||||||
|
if @waiting_for_key
|
||||||
|
if [Gosu::KB_ESCAPE].include?(id)
|
||||||
|
@waiting_for_key = nil
|
||||||
|
return
|
||||||
|
end
|
||||||
|
Config.set_key(@waiting_for_key, id)
|
||||||
|
Config.persist!
|
||||||
|
@waiting_for_key = nil
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
case id
|
||||||
|
when Gosu::KB_UP
|
||||||
|
@selected = (@selected - 1) % (Config.bindings.size + 1)
|
||||||
|
when Gosu::KB_DOWN
|
||||||
|
@selected = (@selected + 1) % (Config.bindings.size + 1)
|
||||||
|
when Gosu::KB_RETURN
|
||||||
|
if @selected == 0
|
||||||
|
Config.debug = !Config.debug
|
||||||
|
Config.persist!
|
||||||
|
else
|
||||||
|
@waiting_for_key = Config.bindings.keys[@selected]
|
||||||
|
end
|
||||||
|
when Gosu::KB_ESCAPE
|
||||||
|
Config.persist!
|
||||||
|
$bus.emit(:change_scene, Menu)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def center_x(text)
|
||||||
|
(SCREEN_SIZE[0] - @font.text_width(text)) / 2
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -17,21 +17,6 @@ class EventBus
|
|||||||
@events[event] << { callback: callback, owner: owner }
|
@events[event] << { callback: callback, owner: owner }
|
||||||
end
|
end
|
||||||
|
|
||||||
# Subscribe to an event that should only be called once
|
|
||||||
def once(event, owner = nil, &callback)
|
|
||||||
wrapper = nil
|
|
||||||
wrapper = proc do |*args|
|
|
||||||
callback.call(*args)
|
|
||||||
off(event, &wrapper)
|
|
||||||
end
|
|
||||||
on(event, owner: owner, &wrapper)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Unsubscribe from an event
|
|
||||||
def off(event, &callback)
|
|
||||||
@events[event].reject! { |h| h[:callback] == callback }
|
|
||||||
end
|
|
||||||
|
|
||||||
# For events that return a value (e.g. queries), get the first non-nil response
|
# For events that return a value (e.g. queries), get the first non-nil response
|
||||||
def get(event, *args)
|
def get(event, *args)
|
||||||
@events[event].dup.each do |h|
|
@events[event].dup.each do |h|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ class MazeData
|
|||||||
|
|
||||||
@boss_rooms = []
|
@boss_rooms = []
|
||||||
@start_room = nil
|
@start_room = nil
|
||||||
|
@exit_room = nil
|
||||||
|
|
||||||
crate_rooms!
|
crate_rooms!
|
||||||
carve_passages_from(0, 0)
|
carve_passages_from(0, 0)
|
||||||
@@ -19,19 +20,36 @@ class MazeData
|
|||||||
next @start_room
|
next @start_room
|
||||||
end
|
end
|
||||||
|
|
||||||
|
$bus.on(:exit_room_coords) do
|
||||||
|
next @exit_room
|
||||||
|
end
|
||||||
|
|
||||||
$bus.on(:room?) do |gx, gy|
|
$bus.on(:room?) do |gx, gy|
|
||||||
([@start_room] + @boss_rooms).any? do |rx, ry|
|
([@start_room, @exit_room] + @boss_rooms).any? do |rx, ry|
|
||||||
gx.between?(rx, rx + BOSS_ROOM_SIZE) &&
|
gx.between?(rx, rx + BOSS_ROOM_SIZE) &&
|
||||||
gy.between?(ry, ry + BOSS_ROOM_SIZE)
|
gy.between?(ry, ry + BOSS_ROOM_SIZE)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
$bus.on(:boss_rooms) do
|
||||||
|
next @boss_rooms
|
||||||
|
end
|
||||||
|
|
||||||
|
$bus.on(:maze_wall?) do |gx, gy|
|
||||||
|
next wall_at?(gx, gy)
|
||||||
|
end
|
||||||
|
|
||||||
|
$bus.on(:torch_placed) do |gx, gy|
|
||||||
|
# check for end condition, if torch is placed in exit room, change scene to victory
|
||||||
|
if (gx.between?(@exit_room[0], @exit_room[0] + BOSS_ROOM_SIZE - 1) &&
|
||||||
|
gy.between?(@exit_room[1], @exit_room[1] + BOSS_ROOM_SIZE - 1))
|
||||||
|
$bus.emit(:change_scene, GameOver)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
return unless $bus.get(:settings, :debug)
|
return unless $bus.get(:settings, :debug)
|
||||||
|
|
||||||
print_debug
|
print_debug
|
||||||
$bus.on(:boss_room_coords) do
|
|
||||||
next @boss_rooms.first ? [@boss_rooms.first[0] * 2 + 1, @boss_rooms.first[1] * 2 + 1] : nil
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def solve(x1, y1, x2, y2)
|
def solve(x1, y1, x2, y2)
|
||||||
@@ -112,7 +130,7 @@ class MazeData
|
|||||||
def wall_at?(gx, gy)
|
def wall_at?(gx, gy)
|
||||||
return true if gx == 0 || gy == 0 || gx == width - 1 || gy == height - 1
|
return true if gx == 0 || gy == 0 || gx == width - 1 || gy == height - 1
|
||||||
|
|
||||||
@boss_rooms.each do |rx, ry|
|
(@boss_rooms + [@start_room, @exit_room]).each do |rx, ry|
|
||||||
display_x1 = rx * 2 + 1
|
display_x1 = rx * 2 + 1
|
||||||
display_y1 = ry * 2 + 1
|
display_y1 = ry * 2 + 1
|
||||||
display_x2 = display_x1 + (BOSS_ROOM_SIZE - 1) * 2
|
display_x2 = display_x1 + (BOSS_ROOM_SIZE - 1) * 2
|
||||||
@@ -183,7 +201,7 @@ class MazeData
|
|||||||
end
|
end
|
||||||
|
|
||||||
def fix_room_entrances!
|
def fix_room_entrances!
|
||||||
(@boss_rooms + [@start_room]).each do |x, y|
|
(@boss_rooms + [@start_room, @exit_room]).each do |x, y|
|
||||||
dir = [N, S, E, W].sample
|
dir = [N, S, E, W].sample
|
||||||
cx = x + BOSS_ROOM_SIZE / 2
|
cx = x + BOSS_ROOM_SIZE / 2
|
||||||
cy = y + BOSS_ROOM_SIZE / 2
|
cy = y + BOSS_ROOM_SIZE / 2
|
||||||
@@ -238,8 +256,26 @@ class MazeData
|
|||||||
rooms << [x, y]
|
rooms << [x, y]
|
||||||
end
|
end
|
||||||
|
|
||||||
@start_room = rooms.shift
|
# assign the start and exit room to be the two furthest apart rooms
|
||||||
@boss_rooms = rooms
|
@start_room, @exit_room = furthest_pair(rooms)
|
||||||
|
@boss_rooms = rooms - [@start_room, @exit_room]
|
||||||
|
end
|
||||||
|
|
||||||
|
def furthest_pair(rects)
|
||||||
|
max_dist = -Float::INFINITY
|
||||||
|
pair = nil
|
||||||
|
|
||||||
|
rects.combination(2) do |r1, r2|
|
||||||
|
x1, y1 = r1
|
||||||
|
x2, y2 = r2
|
||||||
|
dist = Math.hypot(x2 - x1, y2 - y1)
|
||||||
|
if dist > max_dist
|
||||||
|
max_dist = dist
|
||||||
|
pair = [r1, r2]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
pair
|
||||||
end
|
end
|
||||||
|
|
||||||
def carve_passages_from(cx, cy)
|
def carve_passages_from(cx, cy)
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
require 'opengl'
|
require 'opengl'
|
||||||
require 'glu'
|
require 'glu'
|
||||||
require 'gosu'
|
require 'gosu'
|
||||||
|
require 'json'
|
||||||
|
|
||||||
require_relative 'settings/configuration'
|
require_relative 'settings/configuration'
|
||||||
require_relative 'utils/utils'
|
require_relative 'utils/utils'
|
||||||
require_relative 'utils/scene'
|
require_relative 'utils/scene'
|
||||||
require_relative 'settings/scene'
|
require_relative 'settings/scene'
|
||||||
|
require_relative 'credits/scene'
|
||||||
require_relative 'menu/scene'
|
require_relative 'menu/scene'
|
||||||
require_relative 'game_over/scene'
|
require_relative 'game_over/scene'
|
||||||
require_relative 'game/scene'
|
require_relative 'game/scene'
|
||||||
@@ -68,13 +70,11 @@ class Window < Gosu::Window
|
|||||||
|
|
||||||
cam_x, cam_y = $bus.get(:camera_pos) || [0, 0]
|
cam_x, cam_y = $bus.get(:camera_pos) || [0, 0]
|
||||||
torch_lights = []
|
torch_lights = []
|
||||||
# Only calculate torch lights if the setting is enabled, to save performance
|
|
||||||
if $bus.get(:settings, :torches_lightup)
|
|
||||||
torches = $bus.get(:nearby_torches,
|
torches = $bus.get(:nearby_torches,
|
||||||
[cam_x - SCREEN_SIZE[0], cam_y - SCREEN_SIZE[1], SCREEN_SIZE[0] * 4, SCREEN_SIZE[1] * 4]
|
[cam_x - SCREEN_SIZE[0], cam_y - SCREEN_SIZE[1], SCREEN_SIZE[0] * 4, SCREEN_SIZE[1] * 4]
|
||||||
) || []
|
) || []
|
||||||
torch_lights = torches.map { |t| [t[0] - cam_x, SCREEN_SIZE[1] - (t[1] - cam_y)] }.flatten
|
torch_lights = torches.map { |t| [t[0] - cam_x, SCREEN_SIZE[1] - (t[1] - cam_y)] }.flatten
|
||||||
end
|
|
||||||
|
|
||||||
lorentz_field = $bus.get(:lorentz_field) || 0.0
|
lorentz_field = $bus.get(:lorentz_field) || 0.0
|
||||||
|
|
||||||
|
|||||||