diff --git a/assets/images/direction.aseprite b/assets/images/direction.aseprite new file mode 100644 index 0000000..bc771d8 Binary files /dev/null and b/assets/images/direction.aseprite differ diff --git a/assets/images/direction.png b/assets/images/direction.png new file mode 100644 index 0000000..9101f2a Binary files /dev/null and b/assets/images/direction.png differ diff --git a/assets/images/game_over_bg.aseprite b/assets/images/game_over_bg.aseprite deleted file mode 100644 index ee6070f..0000000 Binary files a/assets/images/game_over_bg.aseprite and /dev/null differ diff --git a/assets/images/game_over_bg.png b/assets/images/game_over_bg.png deleted file mode 100644 index adf3cf0..0000000 Binary files a/assets/images/game_over_bg.png and /dev/null differ diff --git a/assets/images/game_over_dead_bg.aseprite b/assets/images/game_over_dead_bg.aseprite new file mode 100644 index 0000000..f7597e8 Binary files /dev/null and b/assets/images/game_over_dead_bg.aseprite differ diff --git a/assets/images/game_over_dead_bg.png b/assets/images/game_over_dead_bg.png new file mode 100644 index 0000000..6b52e08 Binary files /dev/null and b/assets/images/game_over_dead_bg.png differ diff --git a/assets/images/game_over_victory_bg.aseprite b/assets/images/game_over_victory_bg.aseprite new file mode 100644 index 0000000..965c05d Binary files /dev/null and b/assets/images/game_over_victory_bg.aseprite differ diff --git a/assets/images/game_over_victory_bg.png b/assets/images/game_over_victory_bg.png new file mode 100644 index 0000000..bcdaed4 Binary files /dev/null and b/assets/images/game_over_victory_bg.png differ diff --git a/assets/images/heart.aseprite b/assets/images/heart.aseprite new file mode 100644 index 0000000..a5174e5 Binary files /dev/null and b/assets/images/heart.aseprite differ diff --git a/assets/images/heart.png b/assets/images/heart.png new file mode 100644 index 0000000..c946890 Binary files /dev/null and b/assets/images/heart.png differ diff --git a/assets/images/memory.aseprite b/assets/images/memory.aseprite new file mode 100644 index 0000000..3994b93 Binary files /dev/null and b/assets/images/memory.aseprite differ diff --git a/assets/images/memory.png b/assets/images/memory.png new file mode 100644 index 0000000..b400f86 Binary files /dev/null and b/assets/images/memory.png differ diff --git a/game/character.rb b/game/character.rb index 612e629..c31be6a 100644 --- a/game/character.rb +++ b/game/character.rb @@ -126,11 +126,11 @@ class Character $bus.emit(:trap_stepped_on, rect, :character) end - # teleport to boss room for debug purposes + # 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 diff --git a/game/hud/health.rb b/game/hud/health.rb index b9a476f..a1e81de 100644 --- a/game/hud/health.rb +++ b/game/hud/health.rb @@ -5,6 +5,8 @@ class HealthBar @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 @@ -21,6 +23,7 @@ class HealthBar @health -= amount if @health <= 0 @health = 0 + $is_dead = true $bus.emit(:change_scene, GameOver) end end @@ -32,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) # 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) + + @heart_image.draw(SCREEN_SIZE[0] - 100 - 20, 173, 1, 1.5, 1.5) end end \ No newline at end of file diff --git a/game/hud/layer.rb b/game/hud/layer.rb index e0b22d0..21ef28d 100644 --- a/game/hud/layer.rb +++ b/game/hud/layer.rb @@ -1,6 +1,7 @@ require_relative "inventory" require_relative "health" require_relative "logs" +require_relative "minimap" class HUDLayer def initialize @@ -9,6 +10,9 @@ class HUDLayer @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) @@ -26,6 +30,19 @@ class HUDLayer @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 diff --git a/game/hud/minimap.rb b/game/hud/minimap.rb new file mode 100644 index 0000000..14dd904 --- /dev/null +++ b/game/hud/minimap.rb @@ -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 \ No newline at end of file diff --git a/game/props/handler.rb b/game/props/handler.rb index eb4c2e5..e7e4cb8 100644 --- a/game/props/handler.rb +++ b/game/props/handler.rb @@ -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], @@ -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] diff --git a/game/props/torch.rb b/game/props/torch.rb index 3aeeeda..aa7a5ac 100644 --- a/game/props/torch.rb +++ b/game/props/torch.rb @@ -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 \ No newline at end of file diff --git a/game/scene.rb b/game/scene.rb index d772d34..e92ecb0 100644 --- a/game/scene.rb +++ b/game/scene.rb @@ -9,6 +9,8 @@ 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) @@ -94,6 +96,8 @@ class Game < Scene end def button_down(id, pos) + # 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.new) if id == Gosu::KB_ESCAPE return if @hud.button_down(id, pos) diff --git a/game/weapons/blade_of_recursion.rb b/game/weapons/blade_of_recursion.rb index 1e096af..77dfa1d 100644 --- a/game/weapons/blade_of_recursion.rb +++ b/game/weapons/blade_of_recursion.rb @@ -11,6 +11,8 @@ class BladeOfRecursion # 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) @@ -72,10 +74,9 @@ class BladeOfRecursion # this should be in the HUD layer but due to time constraints im putting it here, sorry bar_width = 100 bar_height = 10 - padding = 10 - bar_x = SCREEN_SIZE[0] - bar_width - padding - bar_y = padding + 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) @@ -84,5 +85,7 @@ class BladeOfRecursion 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 \ No newline at end of file diff --git a/game_over/scene.rb b/game_over/scene.rb index 727015a..cb9eed0 100644 --- a/game_over/scene.rb +++ b/game_over/scene.rb @@ -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 diff --git a/settings/scene.rb b/settings/scene.rb index bca37e1..107a23b 100644 --- a/settings/scene.rb +++ b/settings/scene.rb @@ -10,6 +10,6 @@ class Settings < Scene end def button_down(id, _pos) - $bus.emit(:change_scene, Menu.new) if id == Gosu::KB_ESCAPE + $bus.emit(:change_scene, Menu) if id == Gosu::KB_ESCAPE end end \ No newline at end of file diff --git a/todo.md b/todo.md deleted file mode 100644 index 2a29855..0000000 --- a/todo.md +++ /dev/null @@ -1,16 +0,0 @@ -So, - -#landmines and event horizon should be easy - done - -#make enemy spawn in the boss rooms with randomness + enemy count - done -#make the lizard kind of enemy with very high health + dps but slow - done - -#make blade work as planned - done -#make bow quantum - done - -make minimap for the torches revealing teh maze -click on minimap makes game pause to allow pausing for safety - -make radars find exit if even one placed, and make it expensive - -then start writing every mistake and time terribility \ No newline at end of file diff --git a/utils/maze.rb b/utils/maze.rb index 271d333..a6b8aac 100644 --- a/utils/maze.rb +++ b/utils/maze.rb @@ -19,8 +19,12 @@ 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 @@ -30,6 +34,18 @@ class MazeData 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 @@ -113,7 +129,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 @@ -184,7 +200,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 @@ -239,8 +255,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)