mygame/lib/mygame.rb

require 'sdl'

module MyGame
  module Key
    include SDL::Key
  end

  class Wave
    @@wave = {}
    def initialize(fname, ch = :auto, loop = 1)
      @ch = ch
      @loop = loop
      @fname = fname
      self.class.load(fname)
    end

    def self.load(fname)
      @@wave[fname] = SDL::Mixer::Wave.load(fname)
    end

    def self.clear_cache
      @@wave = {}
    end

    def self._loops(loop)
      if loop.nil?
        0
      elsif loop <= 0
        -1
      else
        loop - 1
      end
    end

    def self._ch(ch)
      if ch.nil? or ch == :auto
        -1
      else
        ch
      end
    end

    def self._play(ch, wave, loop)
      SDL::Mixer.play_channel(_ch(ch), wave, _loops(loop))
    end

    def self.play(fname, ch = :auto, loop = 1)
      _play(ch, load(fname), loop)
    end

    def play(ch = @ch, loop = @loop)
      self.class._play(ch, @@wave[@fname], loop)
    end
  end

  class Music < Wave
    def self.load(fname)
      @@wave[fname] = SDL::Mixer::Music.load(fname)
    end

    def _play(ch, wave, loop)
      SDL::Mixer.play_channel(ch, wave, loop)
    end
  end

  class DrawPrim
    attr_accessor :screen
    attr_accessor :x, :y, :w, :h
    attr_accessor :alpha, :hide
    def self.draw(*args)
      new(*args).draw
    end

    def initialize(*argv)
      @screen = @@screen
      @hide = false
      @alpha = 255
    end
  end

  class Image < DrawPrim
    @@image_buf = {}
    attr_accessor :angle, :scale
    attr_reader :image

    def cx; x + w / 2; end
    def cy; y + h / 2; end

    def hit?(target)
      target.x >= x and target.x < x + w and
      target.y >= y and target.y < y + h
    end

    def initialize(fname = nil, x = 0, y = 0)
      super
      @tx = @ty = nil unless defined?(@tx) or defined?(@ty)
      load(fname) if fname
      @x, @y = x, y
      @ox = @oy = 0
      @angle = 0
      @scale = 1
      @anim_labels = {}
      @anim_active = nil
      @anim_counter = 0
    end

    def update_anim
      if anim = @anim_labels[@anim_active]
        size = anim[:patten].size
        idx = anim[:patten][@anim_counter / anim[:time] % size]
        num = @image.w / @w
        @ox = idx % num * @w
        @oy = idx / num * @h
        @anim_counter += 1
      end
    end

    def run
      update_anim
    end

    def set_anim(label, time, patten=nil, loop=nil)
      @anim_labels[label] = {
        :time   => [time, 1].max,
        :patten => patten,
        :loop   => loop,
      }
    end

    def start_anim(label, restart = false)
      return if @anim_active == label and !restart
      @anim_active = label
      @anim_counter = 0
    end

    def stop_anim(label)
      @anim_active = nil
    end

    def load(fname)
      unless @image = @@image_buf[fname]
        @image = SDL::Surface.load(fname).display_format
        set_color_key(@tx, @ty) if @tx and @ty
        @@image_buf[fname] = @image
      end
      @w, @h = @image.w, @image.h
      @image
    end

    def add_load(fname)
      return load(fname) unless @image
      old_image = @image
      new_image = load(fname)
      p w = [old_image.w, new_image.w].max
      p h = old_image.h + new_image.h
      @image = SDL::Surface.new(SDL::SWSURFACE, w, h, 32, *mask_rgba)
      SDL.blit_surface old_image, 0, 0, 0, 0, @image, 0, 0
      SDL.blit_surface new_image, 0, 0, 0, 0, @image, 0, old_image.h
      @image = @image.display_format
      @w, @h = @image.w, @image.h
      @image
    end

    def set_color_key(x = 0, y = 0)
      @image.set_color_key SDL::SRCCOLORKEY, @image.getPixel(x, y)
      @image = @image.display_format
    end

    def draw(x = @x, y = @y)
      return if hide or @image.nil?
      @image.set_alpha(SDL::SRCALPHA,  alpha) if alpha < 255
      if scale == 1 and angle == 0
        SDL.blit_surface @image, @ox, @oy, @w, @h, screen, x, y
      else
        SDL.transform_blit(@image, screen, @angle, @scale, @scale, @w/2, @h/2, x, y, 0)
      end
    end

    def self.clear_cache
      @@image_buf = {}
    end
  end

  class TImage < Image
    def initialize(fname = nil, x = 0, y = 0, tx = 0, ty = 0)
      @tx, @ty = tx, tx
      super fname, x, y
    end
  end

  require 'kconv'
  require 'rbconfig'

  class Font < DrawPrim
    def self.setup_default_setting(ttf = nil, size = nil)
      datadir = Config::CONFIG["datadir"]
      mygame_datadir = File.join(datadir, 'mygame')
      @@default_ttf_path = ttf || File.join(mygame_datadir, 'M+2VM+IPAG-circle.ttf')
      @@default_size = size || 16
    end
    setup_default_setting

    def self.default_size
      @@default_size
    end

    def self.default_size=(size)
      @@default_size = size
    end

    def self.default_ttf_path
      @@default_ttf_path
    end

    def self.default_ttf_path=(size)
      @@default_ttf_path = size
    end

    attr_accessor :added_width
    attr_reader   :str
    def initialize(_str = '', x = 0, y = 0, color = [255, 255, 255],
                   size=self.class.default_size, ttf_path=self.class.default_ttf_path)
      super
      @x = x
      @y = y
      @color = color
      @size = size
      @ttf_path = ttf_path
      @font = open_tff(@ttf_path, @size)
      @font.style = SDL::TTF::STYLE_NORMAL
      @disp_length = nil
      @last_str = nil
      self.str = _str
    end

    def open_tff(ttf_path, size)
      SDL::TTF.open(ttf_path, size)
    end

    def hit?(target)
      target.x >= x and target.x < x + w and
      target.y >= y and target.y < y + h
    end

    %w(color shadow_color size ttf_path).each do |e|
      attr_reader e
      eval "def #{e}=_#{e}
              return if _#{e} == @last_#{e}
              @last_#{e} = _#{e}
              @#{e} = _#{e}
              @font = open_tff(@ttf_path, @size)
              create_surface
            end"
    end

    def str=(_str)
      _str = _str.to_s
      return if _str == @last_str
      @last_str = _str
      @str = Kconv.toutf8(_str)
      create_surface
    end

    def create_surface
      @w, @h = @font.text_size(@str)
      @dx, @dy = if @shadow_color
                   [1 + @size / 24, 1 + @size / 24]
                 else
                   [0, 0]
                 end
      @surface = SDL::Surface.new(SDL::SWSURFACE, w + @dx, h + @dy, 32, *mask_rgba)
      if @shadow_color
        @font.drawSolidUTF8(@surface, @str, @dx, @dy, *@shadow_color)
        @font.drawSolidUTF8(@surface, @str, 0, @dy, *@shadow_color)
      end
      @font.drawSolidUTF8(@surface, @str, 0, 0, *@color)
      @surface.set_color_key SDL::SRCCOLORKEY, @surface.getPixel(0, 0)
      @surface = @surface.display_format
    end

    def start_effect(w)
      @added_width = w
      @disp_length = 0
    end

    def disp_length_max?
      @disp_length.nil? or @disp_length >= str.length
    end

    def run
      @disp_length += @added_width unless disp_length_max?
    end

    def draw(x = @x, y = @y)
      return if hide or @surface.nil?
      if disp_length_max?
        disp_w = 0
        disp_h = 0
      else
        disp_w = @disp_length / 2 * size
        disp_h = h + @dy
      end
      SDL.blit_surface @surface, 0, 0, disp_w, disp_h, screen, x, y
   end
  end

  class SFont < Font
    def initialize *args
      @shadow_color = [32, 32, 32]
      super
    end
  end

  class Square < DrawPrim
    attr_accessor :alpha, :hide
    def initialize(*args)
      @args = args
      @alpha = args[5] || 255
      @fill = false
    end

    def draw(x = @args[0], y = @args[1])
      return if hide
      if alpha < 255
        @args[5] = alpha
        args = [x, y, *@args[2, 4]]
        @fill ? @@screen.draw_filled_rect_alpha(*args) : @@screen.draw_rect_alpha(*args)
      else
        args = [x, y, *@args[2, 3]]
        @fill ? @@screen.fill_rect(*args) : @@screen.draw_rect(*args)
      end
    end
  end

  class FillSquare < Square
    def initialize(*args)
      super
      @fill = true
    end
  end

  def init(flags = SDL::INIT_AUDIO | SDL::INIT_VIDEO)
    raise if SDL.inited_system(flags) > 0
    init_events
    SDL.init flags
    SDL::Mixer.open(22050 * 4) if flags & SDL::INIT_AUDIO
    SDL::Mixer.allocate_channels(16)
    SDL::TTF.init
    @@background_color = [0, 0, 0]
    @@press_last_key ||= {}
  end

  def quit
    SDL.quit
  end

  def create_screen(screenw = 640, screenh = 480, bpp = 16, flags = SDL::SWSURFACE)
    screen = SDL.set_video_mode(screenw, screenh, bpp, flags)
    def screen.update x=0, y=0, w=0, h=0
      self.update_rect x, y, w, h
    end
    @@screen = screen
  end

  def init_game(*args)
    init
    create_screen(*args)
  end

  def main_loop(fps = 60)
    @@ran_loop = true
    @@real_fps = 0

    do_wait = true
    @@count = 0
    @@tm_start = @@ticks = SDL.get_ticks

    until @@loop_end
      poll_event
      SDL::Key.scan
      if block_given?
        screen.fillRect 0, 0, screen.w, screen.h, background_color if background_color
        yield screen
      end
      sync(fps) if do_wait
      screen.flip
    end
  end

  def poll_event
    while event = SDL::Event2.poll
      event.class.name =~ /\w+\z/
      name = $&.gsub(/([a-z])([A-Z])/) { "#{$1}_#{$2.downcase}" }.downcase
      (@@events[name.to_sym] || {}).each {|key, block| block.call(event) }
    end
  end

  def sync(fps)
    diff = @@ticks + (1000 / fps) - SDL.get_ticks
    SDL.delay(diff) if diff > 0

    @@ticks = SDL.get_ticks
    @@count += 1
    if @@count >= 30
      @@count = 0
      @@real_fps = 30 * 1000 / (@@ticks - @@tm_start)
      @@tm_start = @@ticks
    end
  end

  def real_fps
    @@real_fps
  end

  def ran_loop?
    @@ran_loop
  end

  def screen
    @@screen
  end

  def background_color
    @@background_color
  end

  def background_color=(color)
    @@background_color = color
  end

  def add_event(event, key=nil, &block)
    @@events[event] || raise("unknown event type `#{event}'")
    @@events[event][key || block.object_id] = block
  end

  def remove_event(event, key=nil)
    if key
      @@events[event].delete(key)
    else
      @@events[event].each {|key, | @@events[event].delete(key) }
    end
  end

  def init_events
    %w(active key_down key_up mouse_motion mouse_button_down mouse_button_up
       joy_axis joy_ball joy_hat joy_button_up joy_button_down
       quit sys_wm video_resize).each {|e| @@events[e.to_sym] = {} }
    add_event(:quit, :close) { @@loop_end = true }
    add_event(:key_down, :close) {|e| @@loop_end = true if e.sym == Key::ESCAPE }
  end

  def press_key?(key)
    SDL::Key.press?(key)
  end

  def press_new_key?(key)
    flag = @@press_last_key[key] == false && SDL::Key.press?(key)
    @@press_last_key[key] = SDL::Key.press?(key)
    flag
  end

  def mask_rgba
    masks = [0x000000ff, 0x0000ff00, 0x00ff0000, 0xff000000]
    masks.reverse! if big_endian = ([1].pack("N") == [1].pack("L"))
    masks
  end

  @@screen = nil
  @@ran_loop = false
  @@loop_end = false
  @@events = {}
  module_function :real_fps
  module_function :init
  module_function :quit
  module_function :create_screen
  module_function :init_game
  module_function :main_loop
  module_function :ran_loop?
  module_function :screen
  module_function :background_color
  module_function :background_color=
  module_function :add_event
  module_function :remove_event
  module_function :init_events
end