From 7c35419026024ff19f079a136dfcd722039336ce Mon Sep 17 00:00:00 2001 From: Daanish Date: Thu, 2 Apr 2026 22:46:24 +0100 Subject: [PATCH] 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. --- assets/images/event_horizon.aseprite | Bin 0 -> 538 bytes assets/images/event_horizon.png | Bin 0 -> 505 bytes assets/images/landmine.aseprite | Bin 0 -> 526 bytes assets/images/landmine.png | Bin 0 -> 453 bytes assets/images/shrodingers_mine.aseprite | Bin 0 -> 507 bytes assets/images/shrodingers_mine.png | Bin 0 -> 367 bytes game/character.rb | 4 + game/enemy/ai.rb | 4 +- game/enemy/base.rb | 34 ++++++- game/hud/health.rb | 11 ++ game/scene.rb | 16 +++ game/traps/base.rb | 30 ++++++ game/traps/event_horizon.rb | 15 +++ game/traps/handler.rb | 129 ++++++++++++++++++++++++ game/traps/landmine.rb | 13 +++ game/traps/shrodingers_mine.rb | 16 +++ game/weapons/handler.rb | 2 +- game/weapons/lorentz_field.rb | 13 ++- game_over/scene.rb | 4 +- todo.md | 16 +++ 20 files changed, 294 insertions(+), 13 deletions(-) create mode 100644 assets/images/event_horizon.aseprite create mode 100644 assets/images/event_horizon.png create mode 100644 assets/images/landmine.aseprite create mode 100644 assets/images/landmine.png create mode 100644 assets/images/shrodingers_mine.aseprite create mode 100644 assets/images/shrodingers_mine.png create mode 100644 todo.md diff --git a/assets/images/event_horizon.aseprite b/assets/images/event_horizon.aseprite new file mode 100644 index 0000000000000000000000000000000000000000..62da2c8a62a8c94b0b672615ac731cf8d8cd3f1c GIT binary patch literal 538 zcmb1QVqkc%l#xM%L4-kpfsuiMA%y`MC@?UB1Q`Sv1dxTP#-7ESz_Vy(P2~E*?50hvAk3ak5^zA?Y|No!8c6&=*duCQvWLQv{Q`Z_J zqnqK$*Q{dQ;QS~Uo$W;uqrSxFfuSQ{AYrx5@HZy zsF;(SaDZhI)1&_%6Lq3CFinX1tXY}BbGiGB`cawi_b>j>+&QtJSV-ybfBzXfn>30i z+~qT3U&b#jKfm(CJBF8z$NtX?Y08`+b0YV}vjy#n9;+9S^p7=E3VS}9A z0t+X8`I?758Z`+o#cbD2ym{iKO!O=lzwJBD9GuEmxYRR3>fljzv-yg*yYu)I|DJdj Ru=P*Z3~6KX8_upJV@V_JkiZ*;i?OKZMDr4 z1?*%GAAfpq(iS#`jHUZjr8a6Uii>A3`z*mbU-s?o>qj{`ojTVo>7I9aZ^6orCVP#E zA%Y+DKY7L_M+NrrG?Yc?X|U_s9KU<^t5@Hy zAJ1u8S8kqn{PyNIKRz{mI(^b;&JF3A53YXfjCqr~R_lcRQKLCOKKxK(Qx2aM^Y*&2 zUjBqWox`1HtK`f+v&3ulnpf?!i`9{`vwpfWlXdY1*YZPa?Keapw_lUGH0aeXmA?!O O3=E#GelF{r5}E*#n&OoJ literal 0 HcmV?d00001 diff --git a/assets/images/landmine.aseprite b/assets/images/landmine.aseprite new file mode 100644 index 0000000000000000000000000000000000000000..089c55f1dc403060a8817a40d06e2eafbe0c71e1 GIT binary patch literal 526 zcmd;MVqkc%l#xM%L4-kpfsuiMA%y`MC@?UB1Q`Sv1dxTP#_nTeVEFZs1#Bw|0|SE? z0|NuQ0@z9!?S*WGMo7{AZwq2 z`IJ2GO#L8l6(bu(#sZr+anf8_0ufQeZ~1`#;D<3_gjKsYMEg_Zb)%SQQu;7#Wxt{xiW;2{8yV zRLogA*_!LHfq+YCi^(zf0`Y>W*L=0aSGvwxQZiBci9lv?v&^Lh22GrYm7*u6NKBY^ z_4fL?xBu^QmS4~lJ*{xw`oOI^M(<7=@3}WWOYGjAKdL(X@{y)8-%`9)d7dP+-YL@Q zJsuU&|IIUUTKe-vJ*5_|4|&)ucgP=qd3;mx#hM51cITcLPdj(<+|g^T-zVHU(dhf! zLo9cV;??w`2mDK`mi*ipcKYV);x2Z+WT&X$mwa2_EWF!#?Z0>A8&6*6$o~fWmTr`r F0{|KQoRI(k literal 0 HcmV?d00001 diff --git a/assets/images/landmine.png b/assets/images/landmine.png new file mode 100644 index 0000000000000000000000000000000000000000..3b6d28a87ae7f5e67e1cc38e5d5f78674c530b71 GIT binary patch literal 453 zcmeAS@N?(olHy`uVBq!ia0y~yU=RUe4mJh`hTcwDUIqpR#^NA%Cx&(BWL`2bFxGpz zIEGmC-koBbeJDVrOWOtRkY~Rrl z%sSo4Yq!XjrWFDu6O>*lEODOgy+}=~;pkBdDx5YHQsW(;Gp1OWTmFGwZioJ+CQB4Ctv$|#{cQ5CCt`V z`=eI5E)gp?H;vDz5PWjroF1R+zT26X-)>>Nae98&Ev@gW=4akM%b6#-!D};T!^hPZ zI3ura{AHV=+{3l`LE6VB5iz_6UVS$xvh0cBTYW#-?0nbZpqD(;mkMoK)osSvT9+p# z(mVZ_F~h2Dr*qz|t>1IM+Dg1^R`fcP8GP!C!q1-H5D*z)!qs&wXWrMCn{s;%W&LdU z?>(Nh`=G1y!tYz1wdQ`*C~D}ttg}P*M@~wpx}M z0pH{G>}}S`3N+SN8fbP0l+XkKZOq86 literal 0 HcmV?d00001 diff --git a/assets/images/shrodingers_mine.aseprite b/assets/images/shrodingers_mine.aseprite new file mode 100644 index 0000000000000000000000000000000000000000..e307fc481b4b2b76f11c9465d69900136068323a GIT binary patch literal 507 zcmey($iVPmDISz_Vy(P2~E*?50hvAk3ak5^zA?Y|No!8c6&=*duCQvWLQv{Q`Z_J zqnqK$*Q{dQ;QS~&oD4BuqrSxFfuSP{AYrx5@HZ! zsF;(SaDd;1^~nEb1IARAvlom@7?O+5GQBMR+V^k%jL6URht7y5m|3PN&SpOJWCk<0 zaLf!5quk*gLG72-oD$nkPmN$rZ2Rf5EXg$I^rxE^ zysjUOigJ{sw_S0SI(NYwE2?-6CPb6k0 zygj6_az&?*jSo-8#+PrOb6@{ZyOrO$NZXj1nVETx8=Fy47muf;BoEs)y#i;$B+2j` zzfH9bk4j@+MM^AatbX`n-hUn*j`wzzCG#EK`~=UPw_ekr!yA71z=4KIW)Ce{6E#+` zr8r+a*t)p-K~-W>#bjQcrVG4Br{}zwykR1*s;T~CQ-PZSY+DX2=a>*A7bMHTFhQ+t VujA^*Wef}q44$rjF6*2UngD7Ilq&!L literal 0 HcmV?d00001 diff --git a/game/character.rb b/game/character.rb index 9409472..612e629 100644 --- a/game/character.rb +++ b/game/character.rb @@ -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) diff --git a/game/enemy/ai.rb b/game/enemy/ai.rb index e2e9955..ee2d7cf 100644 --- a/game/enemy/ai.rb +++ b/game/enemy/ai.rb @@ -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? diff --git a/game/enemy/base.rb b/game/enemy/base.rb index 4ced1ff..bcf05f7 100644 --- a/game/enemy/base.rb +++ b/game/enemy/base.rb @@ -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 diff --git a/game/hud/health.rb b/game/hud/health.rb index 6cd060e..b9a476f 100644 --- a/game/hud/health.rb +++ b/game/hud/health.rb @@ -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) diff --git a/game/scene.rb b/game/scene.rb index 84b9fa7..86ea26a 100644 --- a/game/scene.rb +++ b/game/scene.rb @@ -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 diff --git a/game/traps/base.rb b/game/traps/base.rb index e69de29..e93b7e2 100644 --- a/game/traps/base.rb +++ b/game/traps/base.rb @@ -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 \ No newline at end of file diff --git a/game/traps/event_horizon.rb b/game/traps/event_horizon.rb index e69de29..f6255ea 100644 --- a/game/traps/event_horizon.rb +++ b/game/traps/event_horizon.rb @@ -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 \ No newline at end of file diff --git a/game/traps/handler.rb b/game/traps/handler.rb index e69de29..dab327c 100644 --- a/game/traps/handler.rb +++ b/game/traps/handler.rb @@ -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 \ No newline at end of file diff --git a/game/traps/landmine.rb b/game/traps/landmine.rb index e69de29..c8fe68f 100644 --- a/game/traps/landmine.rb +++ b/game/traps/landmine.rb @@ -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 \ No newline at end of file diff --git a/game/traps/shrodingers_mine.rb b/game/traps/shrodingers_mine.rb index e69de29..cc9760e 100644 --- a/game/traps/shrodingers_mine.rb +++ b/game/traps/shrodingers_mine.rb @@ -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 \ No newline at end of file diff --git a/game/weapons/handler.rb b/game/weapons/handler.rb index 75ba6e2..979ce97 100644 --- a/game/weapons/handler.rb +++ b/game/weapons/handler.rb @@ -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 diff --git a/game/weapons/lorentz_field.rb b/game/weapons/lorentz_field.rb index 21662e8..21e687d 100644 --- a/game/weapons/lorentz_field.rb +++ b/game/weapons/lorentz_field.rb @@ -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 diff --git a/game_over/scene.rb b/game_over/scene.rb index edc1fab..727015a 100644 --- a/game_over/scene.rb +++ b/game_over/scene.rb @@ -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 diff --git a/todo.md b/todo.md new file mode 100644 index 0000000..1c184e1 --- /dev/null +++ b/todo.md @@ -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 \ No newline at end of file