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:
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 505 B |
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: 367 B |
@@ -122,6 +122,10 @@ class Character
|
||||
end
|
||||
end
|
||||
|
||||
if $bus.get_all(:collides?, rect).include?(:trap)
|
||||
$bus.emit(:trap_stepped_on, rect, :character)
|
||||
end
|
||||
|
||||
# teleport to boss room for debug purposes
|
||||
if $bus.get(:settings, :debug) && Gosu.button_down?(Gosu::KB_T)
|
||||
room_coords = $bus.get(:boss_room_coords)
|
||||
|
||||
+2
-2
@@ -15,7 +15,7 @@ class EnemyAI
|
||||
@time_since_attack = ATTACK_COOLDOWN
|
||||
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)
|
||||
return unless distance < AGGRO_RADIUS
|
||||
|
||||
@@ -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?
|
||||
|
||||
+33
-1
@@ -26,6 +26,8 @@ class Enemy
|
||||
@w = 25
|
||||
@h = 30
|
||||
@health = health
|
||||
@stun = 0
|
||||
@last_lock = :none
|
||||
@lorentz = $bus.get(:lorentz_field?) || false
|
||||
|
||||
$bus.on(:lorentz_field!) do |lorentz|
|
||||
@@ -37,6 +39,20 @@ class Enemy
|
||||
$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)
|
||||
@@ -56,8 +72,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, 200)
|
||||
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
|
||||
|
||||
@@ -4,6 +4,17 @@ class HealthBar
|
||||
def initialize(max_health)
|
||||
@max_health = max_health
|
||||
@health = max_health
|
||||
|
||||
$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)
|
||||
|
||||
@@ -3,6 +3,7 @@ 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
|
||||
@@ -11,11 +12,14 @@ class Game < Scene
|
||||
@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
|
||||
|
||||
@@ -28,14 +32,25 @@ 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
|
||||
@traps.draw
|
||||
$bus.emit(:shade) if $bus.get(:settings, :fog)
|
||||
@hud.draw
|
||||
@weapons.draw
|
||||
@@ -71,6 +86,7 @@ class Game < Scene
|
||||
@character.update(dt)
|
||||
@enemies.update(dt)
|
||||
@props.update(dt)
|
||||
@traps.update(dt)
|
||||
@weapons.update(dt)
|
||||
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,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
|
||||
@@ -26,7 +26,7 @@ class WeaponHandler
|
||||
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)
|
||||
mouse_x, mouse_y = $bus.get(:mouse_pos)
|
||||
direction = Math.atan2(mouse_y - player_y, mouse_x - player_x)
|
||||
weapon.attack(direction)
|
||||
end
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
So,
|
||||
|
||||
#landmines and event horizon should be easy - done
|
||||
|
||||
make enemy spawn in the boss rooms with randomness + enemy count
|
||||
make the lizard kind of enemy with very high health + dps but slow
|
||||
|
||||
make blade work as planned
|
||||
make bow just normal
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user