# Midnight 'Roids Rev. 3 (MousePilot)
# omygawshkenas! 2008
# This code is hereby released into the public domain.

class Asteroids
  WIDTH = 700
  HEIGHT = 600
  
  def self.new_game
    @lives = 3
    @level = 0
    @text = nil
    @ship = Ship.new(WIDTH/2, HEIGHT/2)
    @roids_count = 3 
    self.start_new_level
    @paused = false
    @game_over = false
  end
  
  def self.start_new_level
    @level += 1
    unsafe_to_start = true
    if @ship
      while unsafe_to_start do
        Roid.reset
        @roids_count.times {|i| Roid.new(rand(WIDTH), rand(HEIGHT), (rand - 0.5)*2.5, (rand - 0.5)*2.5, Roid::LARGE) }
        unsafe_to_start = Roid.check_for_collision_with(@ship, Ship::UNIT*5)
      end
    end
  end
  
  def self.game_over
    @text = "Game Over"
  end
  
  def self.pause
    if @paused
      @text = nil
      @paused = false
    else
      @text = "   Paused"
      self.draw
      @paused = true
    end
  end
  
  def self.draw
    unless @paused
      @ship.check_for_collision_with_roids if @ship
      Ray.check_for_collision_with_roids
      $app.clear do
        $app.background $app.black
        $app.para("Lives left: #{@lives}", :top => 5, :left => 5, :stroke => $app.gray(0.5), :font => "13px")
        $app.para("Level: #{@level}", :top => 5, :left => WIDTH - 75, :stroke => $app.gray(0.5), :font => "13px")
        $app.title(@text, :top => 240, :left => 80, :stroke => $app.gray(0.15), :font => "bold 100px") if @text
        @ship.draw if @ship
        Ray.draw_all
        Roid.draw_all
        Explosion.draw_all
      end
    end
  end
  
  def self.ship_explodes
    center_x, center_y = @ship.center_x, @ship.center_y
    if @lives > 0
      @lives -= 1
      unsafe_to_start = true
      @ship = Ship.new(WIDTH/2, HEIGHT/2)
      while unsafe_to_start do
        unsafe_to_start = Roid.check_for_collision_with(@ship, Ship::UNIT*5)
        @ship = Ship.new(rand(WIDTH), rand(HEIGHT)) if unsafe_to_start
      end
    else
      @ship = nil
      self.game_over
    end
    50.times do |i|
      direction = ((2 * Math::PI) * i/50)
      Ray.new(center_x, center_y, 10*Math.cos(direction), 10*Math.sin(direction), direction)
    end
  end
  
  def self.maybe_start_new_level
    if Roid.roids_left == 0
      @roids_count += 2
      self.start_new_level
    end
  end
  
  def self.keypress(key)
    @ship.shoot if (key == ' ') && @ship
    self.new_game if key == 'n'
    self.pause if key == 'p'
  end
end

class CelestialBody
  attr_accessor :x, :y, :size
  
  def center_x
    @x + @size/2
  end
  
  def center_y
    @y + @size/2
  end
  
  def initialize(x, y, vel_x, vel_y, size)
    @x, @y, @vel_x, @vel_y, @size = x, y, vel_x, vel_y, size
  end
  
  def intersects_with?(thing, margin = 0)
    return true if Math.sqrt((center_x - thing.center_x)**2 + (center_y - thing.center_y)**2) < (@size/2 + ((thing.size + margin) / 2))
    return false
  end
end

class Roid < CelestialBody
  LARGE = 80
  MEDIUM = 45
  SMALL = 25
  
  def self.add_roid(roid)
    @roids << roid
  end
  
  def self.remove_roid(roid)
    @roids.delete_at(roid)
  end
  
  def self.draw_all
    @roids.each {|roid| roid.draw }
  end
  
  def self.roids_left
    @roids.length
  end
  
  def self.reset
    @roids = []
  end
  
  def initialize(x, y, vel_x, vel_y, size)
    super(x, y, vel_x + (rand - 0.5)*4.5, vel_y + (rand - 0.5)*4.5, size)
    @color = $app.rgb((rand-0.5)*0.2 + 0.15, (rand-0.5)*0.2 + 0.15, (rand-0.5)*0.3+ 0.45, 0.5)
    Roid.add_roid(self)
  end
  
  def draw
    @x += @vel_x; @y += @vel_y
    @x = @x % Asteroids::WIDTH; @y = @y % Asteroids::HEIGHT
    $app.stroke($app.gray(1.0, 0.5))
    $app.fill(@color)
    $app.oval(@x, @y, @size, @size)
  end
  
  def explode(index)
    Roid.remove_roid(index)
    explode_factor = 0
    case @size
    when LARGE
      explode_factor = rand(10) + 15
      [2, 3][rand(2)].times { Roid.new(center_x - MEDIUM/2, center_y - MEDIUM/2, @vel_x, @vel_y, MEDIUM) }
    when MEDIUM
      explode_factor = rand(5) + 5
      [2, 3][rand(2)].times { Roid.new(center_x - SMALL/2, center_y - SMALL/2, @vel_x, @vel_y, SMALL) }
    when SMALL
      explode_factor = rand(3) + 2
      Asteroids.maybe_start_new_level
    end
    explode_factor.times do |i|
      direction = ((2 * Math::PI) * i/explode_factor)
      Explosion.new(center_x, center_y, 4*Math.cos(direction), 4*Math.sin(direction))
    end
  end
  
  def self.check_for_collision_with(thing, margin = 0)
    @roids.each_with_index do |roid, index|
      return [roid, index] if roid.intersects_with?(thing, margin)
    end
    return false
  end
end

class Explosion < CelestialBody
  @explosions = []
  def self.add_explosion(explosion)
    @explosions << explosion
  end
  
  def self.remove(index)
    @explosions.delete_at(index)
  end
  
  def self.draw_all
    @explosions.each_with_index {|explosion, i| explosion.draw(i) }
  end
  
  def initialize(x, y, vel_x, vel_y)
    super( x, y, vel_x + (rand-0.5)*4, vel_y + (rand-0.5)*4, 10)
    @transparency = 1.0
    Explosion.add_explosion(self)
  end
  
  def draw(index)
    @x += @vel_x; @y += @vel_y
    $app.stroke($app.gray(1.0, @transparency))
    color = [(rand-0.5)*0.7 + 0.7, (rand-0.5)*0.7 + 0.7, (rand-0.5)*0.1 + 0.3, @transparency]
    $app.fill($app.rgb(*color))
    $app.oval(@x, @y, @size, @size)
    @transparency -= 0.065
    Explosion.remove(index) if @transparency <= 0
  end
end

class Ship < CelestialBody
  UNIT = 30
  attr_accessor :thrusting
  
  def self.ship
    @@current_ship
  end
  
  def initialize(x, y)
    super(x, y, 0, 0, UNIT)
    @direction = 0
    @glow = 0
    @thrusting = false
    @@current_ship = self
  end
  
  def draw
    nothing, mouse_x, mouse_y = * $app.mouse
    @direction = Math.atan2(mouse_y - @y, mouse_x - @x)
    thrust if @thrusting
    @x += @vel_x; @y += @vel_y
    @x = @x % Asteroids::WIDTH; @y = @y % Asteroids::HEIGHT
    $app.stroke($app.gray(1.0, 0.5))
    $app.fill($app.rgb(0.7, 0.2, 0.2, 0.5))
    $app.oval(@x, @y, UNIT, UNIT)
    $app.oval(@x + 20*Math.cos(@direction) + 10, @y + 20*Math.sin(@direction) + 10, UNIT/3, UNIT/3)
    $app.fill($app.rgb(1.0, 1.0, 0.8, (1.0 - (Ray.counter.to_f / 20.0))))
    $app.oval(@x + 5*Math.cos(@direction) + 12.5, @y + 5*Math.sin(@direction) + 12.5, UNIT/5, UNIT/5)
    if @glow > 0
      $app.stroke($app.gray(1.0, @glow + 0.2))
      $app.fill($app.gray(1.0, @glow))
      $app.oval(@x + 15*Math.cos(@direction - Math::PI + 0.35) + 11, @y + 15*Math.sin(@direction - Math::PI + 0.35) + 11, UNIT/4, UNIT/4)
      $app.oval(@x + 15*Math.cos(@direction - Math::PI - 0.35) + 11, @y + 15*Math.sin(@direction - Math::PI - 0.35) + 11, UNIT/4, UNIT/4)
      @glow -= 0.06
    end
  end
  
  def shoot
    Ray.new(@x + UNIT/2, @y + UNIT/2, @vel_x, @vel_y, @direction) if Ray.counter < 20
  end

  def thrust
    @glow = 0.8
    @vel_x += 0.5*Math.cos(@direction); @vel_y += 0.5*Math.sin(@direction)
    ["@vel_x", "@vel_y"].each do |vel|
      instance_variable_set(vel, 10) if instance_variable_get(vel) > 10
      instance_variable_set(vel, -10) if instance_variable_get(vel) < -10
    end
  end
  
  def check_for_collision_with_roids
    if roid = Roid.check_for_collision_with(self)
      Asteroids.ship_explodes
    end
  end
end

class Ray < CelestialBody
  @rays = []
  @counter = 0
  
  def self.counter
    return @counter
  end
  
  def self.add_ray(ray)
    @rays << ray
    @counter += 1
  end
  
  def self.draw_all
    @rays.each_with_index {|ray, i| ray.draw(i) }
  end
  
  def self.remove_ray(index)
    @rays.delete_at(index)
    @counter -= 1
  end
  
  def self.check_for_collision_with_roids
    @rays.each_with_index do |ray, index| 
      if roid_list = Roid.check_for_collision_with(ray)
        Ray.remove_ray(index)
        roid_list[0].explode(roid_list[1])
      end
    end
  end
  
  def initialize(x, y, vel_x, vel_y, direction)
    @direction = direction
    super(x, y, vel_x + 8 * Math.cos(@direction), vel_y + 8 * Math.sin(@direction), 5)
    Ray.add_ray(self)
  end
  
  def draw(index)
    @x += @vel_x; @y += @vel_y
    if @x > Asteroids::WIDTH || @x < 0 || @y > Asteroids::HEIGHT || @y < 0
      Ray.remove_ray(index)
    else
      $app.stroke($app.rgb(1.0, 1.0, 0.8, 0.8))
      $app.line(@x, @y, @x + @vel_x, @y + @vel_y)
    end
  end
end

Shoes.app :width => Asteroids::WIDTH, :height => Asteroids::HEIGHT, :title => "Asteroids", :resizable => false do
  $app = self
  Asteroids.new_game
  animate(60) { Asteroids.draw }
  keypress {|k| Asteroids.keypress(k) }
  click { Ship.ship.thrusting = true }
  release { Ship.ship.thrusting = false }
end