game.rb 7.75 KB
#!/usr/bin/env ruby

$: << File.join(File.dirname(__FILE__), "lib")

require 'rubygame'
require 'chipmunk'

require 'input_bma180'
require 'drawable'
require 'explosion'
require 'movable'
require 'bullet'
require 'weapon'
require 'ship'
require 'enemy'
require 'player'


Rubygame.init
class Array
  def add(o); calc(o, :+); end
  def sub(o); calc(o, :-); end
  def mul(o); calc(o, :*); end
  def div(o); calc(o, :/); end
  def calc(other, op)
    other = [other, other]  unless other.is_a?(Array)
    zip(other).map {|v| v.inject(op) }
  end
end
class Numeric
  def radians_to_vec2
    CP::Vec2.new(Math::cos(self), Math::sin(self))
  end
end
class Game

  include Rubygame

  attr_reader :screen, :queue, :clock, :objects, :player, :enemies, :level, :opts, :steps
  attr_accessor :state, :space, :options
  def initialize options
    @options = options
    flags = [HWSURFACE, DOUBLEBUF]
    flags << FULLSCREEN  if options[:fullscreen]
    @screen = Screen.new options[:size], 0, flags
    @screen.title = "test-game"
    @space = CP::Space.new
    @space.damping = 1.0
    setup_collisions
    @queue = EventQueue.new
    @clock = Clock.new
    @clock.target_framerate = 60
    @keys_pressed = []
    @background = Surface.load(File.join(data_dir, "images", "background.png"))
      .zoom_to(@screen.size[0], @screen.size[1] * 2)
      .to_display
    @state = :ready
    @opts = opts
    @steps = 0
    Music.autoload_dirs = [ File.join(data_dir, "sound") ]
  end

  def setup_collisions
    @space.add_collision_func(:ship, :ship) do |ship1_shape, ship2_shape|
      ship1 = self.objects.find {|o| o.shape == ship1_shape }
      ship2 = self.objects.find {|o| o.shape == ship2_shape }
      next  unless ship1 && ship2
      power1 = ship1.power
      ship1.take_damage(ship2.power / 10)
      ship2.take_damage(power1 / 10)
    end
    @space.add_collision_func(:ship, :bullet) do |ship_shape, bullet_shape|
      bullet = self.objects.find {|o| o.shape == bullet_shape }
      ship = self.objects.find {|o| o.shape == ship_shape }
      next unless bullet && ship
      next  if bullet.options[:shooter] == ship
      next  if bullet.options[:shooter].is_a?(Enemy) && ship.is_a?(Enemy)
      bullet.hit(ship)
      @space.remove_body(bullet_shape.body)
      @space.remove_shape(bullet_shape)
    end
    @space.add_collision_func(:bullet, :bullet, &nil)
    @space.add_collision_func(:explosion, :explosion, &nil)
    @space.add_collision_func(:explosion, :bullet, &nil)
    @space.add_collision_func(:explosion, :ship, &nil)
  end

  def start_game
    @objects = []
    @enemies = []
    @bg_pos = @background.size[1] / 2
    @level = options[:level]
    @player = Player.new(self, p: [@screen.size[0] / 2, @screen.size[1] - 100])
    @state = :running
    @player.show
    flash_text(text: "Level #{@level}", alpha: 150)
  end

  def run
    start_game
    loop do
      passed = @clock.tick
      if @state != :pause
        @steps += passed
        @space.step(passed)
      end

      @queue.enable_new_style_events
      @queue.each do |event|
        @keys_pressed << event.key       if event.is_a?(Events::KeyPressed)
        @keys_pressed.delete(event.key)  if event.is_a?(Events::KeyReleased)
        @keys_pressed << :space          if event.is_a?(Events::MousePressed)
        @keys_pressed.delete(:space)     if event.is_a?(Events::MouseReleased)
        if event.is_a?(Events::MouseMoved) && !options[:bma180]
          @player.body.p = CP::Vec2.new(*event.pos)
        end
        exit  if event.is_a?(Rubygame::Events::QuitRequested)
        exit  if event.is_a?(Events::KeyPressed) &&
          [:escape, :q].include?(event.key)
      end

      if @keys_pressed.include?(:p)
        if !@pause_pressed_at || Time.now - @pause_pressed_at > 1
          if @state == :pause
            @state = :running
          else
            @state = :pause
            flash_text(text: "PAUSE", alpha: 150)
            draw
          end
          @pause_pressed_at = Time.now
        end
      end

      if @state == :lost && @keys_pressed.include?(:return)
        start_game
      end
      
      if @state != :pause
        @player.body.v += CP::Vec2.new(*BMA180.read)  if options[:bma180]
        
        @player.fire  if @keys_pressed.include?(:space) && @state == :running
        
        Enemy.generate(self)  if @enemies.size < @level
        
        [:up, :down, :left, :right].each {|d|
          @player.move(d) if @keys_pressed.include?(d) }

        draw
        
        # TODO
        if @bg_pos.abs >= (@background.size[1] * 2 * @level) && @state == :running
          @level += 1
          flash_text(text: "Level #{@level}")
          @bg_pos = @background.size[1] / 2
        end
      end
    end
  end

  def draw
    @bg_pos -= 1
    @background.blit(@screen, [0, 0], [0, @bg_pos % @background.size[1] / 2, *@screen.size])

    @objects.each {|o| o.draw(@screen) }
    draw_hud
    draw_debug  if @options[:debug]
    @screen.flip
  end

  def draw_debug
    text = "[ fps: #{"%.2f" % @clock.framerate} | " +
      "objects: #{@objects.count} | pos: #{0 - @bg_pos} | "+
      "level: #{@level} | power: #{@player.power} ]"
    text += "[ #{@keys_pressed.join(' | ')} ]"  if @keys_pressed.any?
    font("FreeSans").render(text, false, :white)
      .blit(@screen, [10, @screen.size[1] - 20])

    ([@player] + @enemies + @objects).each do |ship|
      ship.debug.blit(@screen, ship.pos.add(ship.shape.r)) rescue nil
    end
  end

  def draw_hud
    if @state == :lost
      text = "Game Over"
      surface = font("captain", 150).render(text, false, :red)
      surface.alpha = 150
      surface.blit(@screen, @screen.size.div(2).sub(surface.size.div(2)))
    end

    if @flash
      begin
        surface = font("captain", @flash[:size]).render(@flash[:text], false, @flash[:color])
        surface.alpha = @flash[:alpha]
        surface.blit(@screen, @screen.size.div(2).sub(surface.size.div(2)))
      rescue
      end
    end

    draw_power_bar
    draw_score
  end

  def draw_power_bar
    return  unless @player.power > 1
    surface = Surface.new([(@screen.size[0] / 100 * @player.power) - 10, 20])
    surface.fill :blue
    surface.blit(@screen, [5, @screen.size[1] - 25])
  end

  def draw_score
    text = @player.score.to_s
    color = @flash_hud || :white
    surface = font("captain", 50).render(text, false, color)
    surface.alpha = 150
    surface.blit(@screen, @screen.size.sub([surface.size[0] + 10, 100]))
  end

  def flash_hud color, duration = 1
    @flash_hud = color
    Thread.start { sleep duration; @flash_hud = nil }
  end

  def flash_text opts = {}
    @flash = {color: :white, alpha: 255, size: 100, duration: 3}.merge(opts)
    Thread.start { sleep @flash[:duration]; @flash = nil }
  end

  def data_dir
    File.join(File.dirname(__FILE__), "data")
  end

  def font name, size = 10
    @fonts ||= {}
    @fonts[name.to_sym] ||= TTF.new(File.join(data_dir, "fonts", "#{name}.ttf"), size)
  end
end

Rubygame::TTF.setup

require 'optparse'

@opts = { size: [800, 600],
  fullscreen: false,
  mute: false,
  level: 1,
  debug: false,
  bma180: false,
}

OptionParser.new do |o|
  o.banner = "usage: ruby #{__FILE__} [-l LEVEL] [-d]"

  o.on("-s", "--size SIZE", "Window size (800x600)") do |size|
    @opts[:size] = *size.split(/x|:/).map(&:to_i)
  end

  o.on("-f", "--fullscreen", "Fullscreen mode") do
    @opts[:fullscreen] = true
  end

  o.on("-m", "--mute", "Disable sounds") do
    @opts[:mute] = true
  end

  o.on("-l", "--level LEVEL", "Start at level") do |l|
    @opts[:level] = l.to_i
  end

  o.on("--bma180", "Enable bma180 accelerometer input") do
    @opts[:bma180] = true
  end

  o.on("-d", "--debug", "Debug mode") do
    @opts[:debug] = true
  end
end.parse!


#require 'ruby-prof'
#results = RubyProf.profile do
  Game.new(@opts).run
#end
#printer = RubyProf::GraphHtmlPrinter.new(results)
#printer.print(File.new("report.html","w"))