Compare commits

...

10 Commits

Author SHA1 Message Date
syedm da90a7836e Update gitignore file 2026-07-02 13:12:11 +01:00
syedm 3d4ae43f52 Touchups 2026-04-03 18:10:55 +01:00
syedm 35cddfc53c Enhance enemy AI and weapon mechanics: add knockback effect based on player position in Enemy class; improve attack direction handling and animation in OccamsRazor class.
Basically the finishing touches.
2026-04-03 14:59:28 +01:00
syedm bf1171a775 Refactor input handling: replace hardcoded key inputs with configurable key bindings for character movement, inventory navigation, and actions; enhance settings scene with key rebind functionality and add credits scene. 2026-04-03 14:46:35 +01:00
syedm 5814a1fdee Final commit (i think) 2026-04-03 02:53:01 +01:00
syedm 222b28d196 Add new assets and implement minimap functionality
- Added new image assets for game over screens: `game_over_dead_bg.png`, `game_over_victory_bg.png`, and their respective Aseprite files.
- Introduced a new `Minimap` class to manage torch placement and rendering on the minimap.
- Implemented methods for adding and removing torches, updating the grid area, and drawing the minimap based on the current state of the game.
- Enhanced the minimap display with color coding for walls, empty spaces, and torches.
2026-04-03 02:51:11 +01:00
syedm b325b35c88 Implement logging system: add GameLogger class for managing log messages, integrate logging in HUDLayer and ShrodingersMine for enhanced gameplay feedback. 2026-04-03 00:38:27 +01:00
syedm 94be1f104f Refactor weapon system: update BladeOfRecursion and QuantumBow classes with improved attack mechanics, damage handling, and drawing logic; enhance WeaponHandler for better weapon management. 2026-04-03 00:28:50 +01:00
syedm d0466599df Add Komodo enemy class and enhance enemy spawning logic
- Introduce Komodo class with high health and attack mechanics.
- Modify Enemy class to accept type parameter during initialization.
- Update EnemyAI to set speed based on enemy type.
- Implement random spawning of enemies in boss rooms.
- Adjust health bar calculations to use max health.
2026-04-02 23:15:03 +01:00
syedm 7c35419026 Implement traps system: add Trap, Landmine, and EventHorizon classes with collision detection and interaction logic; integrate trap handling in the game scene and enemy AI. 2026-04-02 22:46:24 +01:00
62 changed files with 970 additions and 134 deletions
+3 -1
View File
@@ -1 +1,3 @@
*.old.*
*.old.*
config.json
p.vim
-7
View File
@@ -1,7 +0,0 @@
{
"editor.tabSize": 2,
"editor.insertSpaces": true,
"editor.fontFamily": "'Operator Mono Lig', monospace",
"editor.fontSize": 14,
"editor.fontLigatures": true,
}
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 233 B

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 352 B

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 162 B

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 505 B

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 187 B

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 453 B

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 186 B

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 386 B

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 367 B

+44
View File
@@ -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
+11 -7
View File
@@ -76,10 +76,10 @@ class Character
dx = 0.0
dy = 0.0
dx -= 1 if Gosu.button_down?(Gosu::KB_A)
dx += 1 if Gosu.button_down?(Gosu::KB_D)
dy -= 1 if Gosu.button_down?(Gosu::KB_W)
dy += 1 if Gosu.button_down?(Gosu::KB_S)
dx -= 1 if Gosu.button_down?($bus.get(:settings, :leftward))
dx += 1 if Gosu.button_down?($bus.get(:settings, :rightward))
dy -= 1 if Gosu.button_down?($bus.get(:settings, :forward))
dy += 1 if Gosu.button_down?($bus.get(:settings, :backward))
moving = dx != 0 || dy != 0
@@ -122,11 +122,15 @@ class Character
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)
room_coords = $bus.get(:boss_room_coords)
room_coords = $bus.get(:exit_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
+13 -13
View File
@@ -1,28 +1,28 @@
class EnemyAI
AGGRO_RADIUS = 2000
BASE_SPEED = 130.0
CORNER_OFFSET = 35 # how far from tile center to place corner waypoint
ATTACK_COOLDOWN = 0.2 # seconds
def initialize(enemy, type)
@enemy = enemy
@type = type
@base_speed = case type
when :komodo then 60
when :force then 130
end
@tile_path = [] # raw tile coords from solver
@waypoints = [] # actual world positions to move through
@last_player_tile = nil
@avoiding_obstacle = false
@speed = BASE_SPEED
@speed = @base_speed
@time_since_attack = ATTACK_COOLDOWN
end
def update(player_x, player_y, dt)
distance = Math.sqrt((player_x - @enemy.x)**2 + (player_y - @enemy.y)**2)
return unless distance < AGGRO_RADIUS
def update(player_x, player_y, dt, force = false)
distance = Math.hypot(@enemy.x - player_x, @enemy.y - player_y)
if @enemy.lorentz && distance < 230
@speed = BASE_SPEED * 0.3
@speed = @base_speed * 0.3
else
@speed = BASE_SPEED
@speed = @base_speed
end
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]
]
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)
@avoiding_obstacle = true
end
@@ -76,7 +76,7 @@ class EnemyAI
end
end
if @waypoints.empty? || @last_player_tile != player_tile
if force || @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(: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 = 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
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) }
second = candidates.min_by { |cx, cy| Math.hypot(cx - next_cx, cy - next_cy) }
waypoints << first
waypoints << second
+45 -4
View File
@@ -20,26 +20,50 @@ class Enemy
[@i_w, @i_h, @s]
end
def initialize(x, y, health = 100)
def initialize(x, y, type, health = 100)
@x = x
@y = y
@w = 25
@h = 30
@max_health = health
@health = health
@type = type
@stun = 0
@last_lock = :none
@lorentz = $bus.get(:lorentz_field?) || false
$bus.on(:lorentz_field!) do |lorentz|
@lorentz = lorentz
end
@ai = EnemyAI.new(self, :chase)
@ai = EnemyAI.new(self, @type)
$bus.on(:collides?) do |rect|
next collides?(rect) ? :enemy : nil
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
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
if @health <= 0
$bus.emit(:enemy_died, self)
@@ -56,8 +80,24 @@ class Enemy
end
def update(dt)
if @stun > 0
@stun -= 1
return
end
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
def draw
@@ -76,7 +116,8 @@ class Enemy
# 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)
if $bus.get(:settings, :debug)
+1 -1
View File
@@ -4,7 +4,7 @@ class Force < Enemy
load 'assets/images/force.png', 21, 32, 1.7
def initialize(x, y)
super(x, y, 100)
super(x, y, :force, 100)
$bus.on(:attack) do |attack_x, attack_y, range, damage|
# Whoa, did'nt know ruby had hypot functions built in!
+6 -4
View File
@@ -1,4 +1,5 @@
require_relative 'force'
require_relative 'komodo'
class EnemyHandler
def initialize
@@ -16,10 +17,11 @@ class EnemyHandler
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? && start_room_coords
@enemies << Force.new((start_room_coords[0] * 2 + 1 + 4) * 60 + 30, (start_room_coords[1] * 2 + 1 + 4) * 60 + 30)
boss_rooms = $bus.get(:boss_rooms) || []
if @enemies.count < 4 && rand < (1.0 / (60 * 10))
boss_room = boss_rooms.sample
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
+17
View File
@@ -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
+18 -2
View File
@@ -4,12 +4,26 @@ class HealthBar
def initialize(max_health)
@max_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
def take_damage(amount)
@health -= amount
if @health <= 0
@health = 0
$is_dead = true
$bus.emit(:change_scene, GameOver)
end
end
@@ -21,10 +35,12 @@ class HealthBar
def draw
# 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
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
+5 -5
View File
@@ -94,19 +94,19 @@ class Inventory
when Gosu::KB_1 then @inventory_selected = :occams_razor
when Gosu::KB_2 then @inventory_selected = :quantum_bow
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)
@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)
@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)
@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)
@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))
else
return false
+25
View File
@@ -1,23 +1,48 @@
require_relative "inventory"
require_relative "health"
require_relative "logs"
require_relative "minimap"
class HUDLayer
def initialize
@hud_bg = Gosu::Image.new("assets/images/hud.png", retro: true)
@hud_fg = Gosu::Image.new("assets/images/hud_fg.png", retro: true)
@inventory = Inventory.new
@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|
@health.take_damage(damage)
end
end
def update(dt)
@logs.update(dt)
end
def draw
@hud_bg.draw(0, 0, Float::INFINITY)
@inventory.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)
end
+47
View File
@@ -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
+92
View File
@@ -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
+4
View File
@@ -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
+1 -1
View File
@@ -49,7 +49,7 @@ class Prop
end
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]
+31 -2
View File
@@ -22,6 +22,34 @@ class PropsHandler
$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] }
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
def spawn_chests!
@@ -49,7 +77,7 @@ class PropsHandler
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]
x, y = [
[start_room_x, start_room_y],
@@ -94,7 +122,7 @@ class PropsHandler
@grid.delete([prop.x, prop.y])
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)
if selected_item == :torch
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 $bus.get_all(:collides?, torch.collision_rect).include?(:character) # can't place if colliding with player
return unless $bus.get(:consume, :torch, 1)
$bus.emit(:torch_placed, torch.x, torch.y)
add(torch)
elsif selected_item == :radar
player_x, player_y = $bus.get(:player_position) || [0, 0]
+9
View File
@@ -6,4 +6,13 @@ class Torch < Prop
def resources
{ wood: rand(0..8) }
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
+26 -3
View File
@@ -3,24 +3,32 @@ require_relative 'hud/layer'
require_relative 'props/handler'
require_relative 'enemy/handler'
require_relative 'weapons/handler'
require_relative 'traps/handler'
require_relative 'character'
class Game < Scene
def initialize
super
$is_dead = false
@font = Gosu::Font.new(24)
@floor_image = Gosu::Image.new("assets/images/floor.png", retro: true)
@blasted = 0
@maze = Maze.new
@character = Character.new
@enemies = EnemyHandler.new
@props = PropsHandler.new
@weapons = WeaponHandler.new
@traps = TrapHandler.new
@hud = HUDLayer.new
@camera = [0, 0]
$bus.emit(:log, "Welcome to the Mist!")
$bus.on(:player_move) do |pos|
@camera = pos
end
@@ -28,15 +36,26 @@ class Game < Scene
$bus.on(:camera_pos) do
next @camera
end
$bus.on(:blast) do |_|
@blasted = 10 # blast for 10 frames
end
end
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
@maze.draw
@character.draw
@enemies.draw
@props.draw
$bus.emit(:shade) if $bus.get(:settings, :fog)
@traps.draw
$bus.emit(:shade)
@hud.draw
@weapons.draw
draw_debug! if $bus.get(:settings, :debug)
@@ -71,15 +90,19 @@ class Game < Scene
@character.update(dt)
@enemies.update(dt)
@props.update(dt)
@traps.update(dt)
@weapons.update(dt)
@hud.update(dt)
end
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 @props.button_down(id, pos)
#return if @weapons.button_down(id, pos)
return if @weapons.button_down(id, pos)
#@character.button_down(id)
end
+30
View File
@@ -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
+15
View File
@@ -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
+129
View File
@@ -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
+13
View File
@@ -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
+18
View File
@@ -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
+68 -7
View File
@@ -1,16 +1,25 @@
class BladeOfRecursion
def initialize
@damage = 1
@range = 200
@speed = 500
@range = 150
@speed = 400
@x = 0
@y = 0
@start_x = 0
@start_y = 0
@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
def attack(direction)
return if @direction
@direction = direction
@x, @y = $bus.get(:player_position)
@start_x, @start_y = @x, @y
end
def update(dt)
@@ -19,12 +28,64 @@ class BladeOfRecursion
@x += Math.cos(@direction) * @speed * dt
@y += Math.sin(@direction) * @speed * dt
$bus.get(:attack, @x, @y, @range, @damage)
# 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
if Math.hypot(@x - @start_x, @y - @start_y) > @range
@direction = nil
@damage = 1
return
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
+26 -20
View File
@@ -1,39 +1,45 @@
require_relative 'lorentz_field'
require_relative 'occams_razor'
#require_relative 'quantum_bow'
#require_relative 'blade_of_recursion'
require_relative 'quantum_bow'
require_relative 'blade_of_recursion'
class WeaponHandler
def initialize
@weapons = {
lorentz_field: LorentzField.new,
occams_razor: OccamsRazor.new,
#quantum_bow: QuantumBow.new,
#blade_of_recursion: BladeOfRecursion.new
quantum_bow: QuantumBow.new,
blade_of_recursion: BladeOfRecursion.new
}
end
def 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
def draw
@weapons.each_value(&:draw)
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
+6 -7
View File
@@ -1,17 +1,16 @@
class LorentzField
BASE_TIME = 25 # Base time for the field to be active in seconds
def initialize(bus)
@bus = bus
def initialize
@time = 0
@active = false
@font = Gosu::Font.new(24, name: "assets/fonts/tn.ttf")
@bus.on(:lorentz_field?) do
$bus.on(:lorentz_field?) do
next @active
end
@bus.on(:lorentz_field) do
$bus.on(:lorentz_field) do
if @active
t = @time
remaining = BASE_TIME - t
@@ -34,10 +33,10 @@ class LorentzField
def attack(_direction)
return if @active
@bus.emit(:consume, :lorentz_field, 1)
$bus.emit(:consume, :lorentz_field, 1)
@active = true
@time = 0
@bus.emit(:lorentz_field!, true)
$bus.emit(:lorentz_field!, true)
end
def update(dt)
@@ -47,7 +46,7 @@ class LorentzField
if @time >= BASE_TIME
@time = 0
@active = false
@bus.emit(:lorentz_field!, false)
$bus.emit(:lorentz_field!, false)
end
end
+30 -2
View File
@@ -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
end
def attack(_direction)
def attack(direction)
return if @active
@active = true
@time = BASE_TIME
@direction = direction # radians
@spread = Math::PI # 90° total arc
# Check for enemies in range and deal damage
player_x, player_y = $bus.get(:player_position)
$bus.emit(:attack, player_x, player_y, @range, @damage)
@@ -23,6 +26,10 @@ class OccamsRazor
return unless @active
@time -= dt
progress = @time / BASE_TIME
@spread = (Math::PI) * progress
if @time <= 0
@active = false
@time = 0
@@ -30,6 +37,27 @@ class OccamsRazor
end
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
+79
View File
@@ -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
+3 -3
View File
@@ -8,7 +8,7 @@ class GameOver < Scene
super
@font = Gosu::Font.new(32, name: "assets/fonts/tn.ttf")
@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
end
@@ -27,7 +27,7 @@ class GameOver < Scene
@bg_image.draw(0, 0, 1)
OPTIONS.each_with_index do |(text, _), index|
y = 260 + index * 50
y = 360 + index * 50
if index == @selected_index
@font.draw_text("[ #{text.ljust(10)} ]", 50, y, 1, 1, 1, Gosu::Color::YELLOW)
else
@@ -56,7 +56,7 @@ class GameOver < Scene
def item_at(pos)
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))
return [index, action]
end
+1
View File
@@ -2,6 +2,7 @@ class Menu < Scene
OPTIONS = {
"Start" => -> { $bus.emit(:change_scene, Game) },
"Settings" => -> { $bus.emit(:change_scene, Settings) },
"Credits" => -> { $bus.emit(:change_scene, Credits) },
"Quit" => -> { $bus.emit(:quit_game) }
}.freeze
+38 -11
View File
@@ -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
module Config
@config = {
debug: ARGV.include?("--debug"),
fog: true,
torches_lightup: true,
@debug = false
@keybinds = {
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")
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
$bus.on(:settings, :master) { |k| @config[k] }
$bus.on(:settings, :master) { |k| k == :debug ? @debug : @keybinds[k] }
module_function
def toggle(key)
@config[key] = !@config[key]
def bindings
@keybinds
end
def debug
@debug
end
def debug=(value)
@debug = value
end
def set_key(action, key)
@keybinds[action] = key
end
def persist!
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
+65 -4
View File
@@ -1,15 +1,76 @@
class Settings < Scene
def initialize
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
def draw
Gosu.draw_rect(0, 0, SCREEN_SIZE[0], SCREEN_SIZE[1], Gosu::Color::BLACK)
@font.draw_text("Settings Scene - Press ESC to return to Menu", 50, 50, 1, 1, 1, Gosu::Color::WHITE)
Gosu.draw_rect(0, 0, *SCREEN_SIZE, Gosu::Color.new(0xFF131313))
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
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
-15
View File
@@ -17,21 +17,6 @@ class EventBus
@events[event] << { callback: callback, owner: owner }
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
def get(event, *args)
@events[event].dup.each do |h|
+44 -8
View File
@@ -7,6 +7,7 @@ class MazeData
@boss_rooms = []
@start_room = nil
@exit_room = nil
crate_rooms!
carve_passages_from(0, 0)
@@ -19,19 +20,36 @@ class MazeData
next @start_room
end
$bus.on(:exit_room_coords) do
next @exit_room
end
$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) &&
gy.between?(ry, ry + BOSS_ROOM_SIZE)
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)
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
def solve(x1, y1, x2, y2)
@@ -112,7 +130,7 @@ class MazeData
def wall_at?(gx, gy)
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_y1 = ry * 2 + 1
display_x2 = display_x1 + (BOSS_ROOM_SIZE - 1) * 2
@@ -183,7 +201,7 @@ class MazeData
end
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
cx = x + BOSS_ROOM_SIZE / 2
cy = y + BOSS_ROOM_SIZE / 2
@@ -238,8 +256,26 @@ class MazeData
rooms << [x, y]
end
@start_room = rooms.shift
@boss_rooms = rooms
# assign the start and exit room to be the two furthest apart 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
def carve_passages_from(cx, cy)
+7 -7
View File
@@ -1,11 +1,13 @@
require 'opengl'
require 'glu'
require 'gosu'
require 'json'
require_relative 'settings/configuration'
require_relative 'utils/utils'
require_relative 'utils/scene'
require_relative 'settings/scene'
require_relative 'credits/scene'
require_relative 'menu/scene'
require_relative 'game_over/scene'
require_relative 'game/scene'
@@ -68,13 +70,11 @@ class Window < Gosu::Window
cam_x, cam_y = $bus.get(:camera_pos) || [0, 0]
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,
[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
end
torches = $bus.get(:nearby_torches,
[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
lorentz_field = $bus.get(:lorentz_field) || 0.0