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.

This commit is contained in:
2026-04-02 22:46:24 +01:00
parent db7c2aa7a2
commit 7c35419026
20 changed files with 294 additions and 13 deletions
+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
+16
View File
@@ -0,0 +1,16 @@
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
end
true # to destroy the trap
end
end