game_2d 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +15 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +84 -0
- data/Rakefile +1 -0
- data/bin/game_2d_client.rb +17 -0
- data/bin/game_2d_server.rb +21 -0
- data/game_2d.gemspec +32 -0
- data/lib/game_2d/client_connection.rb +127 -0
- data/lib/game_2d/client_engine.rb +227 -0
- data/lib/game_2d/complex_move.rb +45 -0
- data/lib/game_2d/entity.rb +371 -0
- data/lib/game_2d/entity/block.rb +73 -0
- data/lib/game_2d/entity/owned_entity.rb +29 -0
- data/lib/game_2d/entity/pellet.rb +27 -0
- data/lib/game_2d/entity/titanium.rb +11 -0
- data/lib/game_2d/entity_constants.rb +14 -0
- data/lib/game_2d/game.rb +213 -0
- data/lib/game_2d/game_space.rb +462 -0
- data/lib/game_2d/game_window.rb +260 -0
- data/lib/game_2d/hash.rb +11 -0
- data/lib/game_2d/menu.rb +82 -0
- data/lib/game_2d/move/rise_up.rb +77 -0
- data/lib/game_2d/player.rb +251 -0
- data/lib/game_2d/registerable.rb +25 -0
- data/lib/game_2d/serializable.rb +69 -0
- data/lib/game_2d/server_connection.rb +104 -0
- data/lib/game_2d/server_port.rb +74 -0
- data/lib/game_2d/storage.rb +42 -0
- data/lib/game_2d/version.rb +3 -0
- data/lib/game_2d/wall.rb +21 -0
- data/lib/game_2d/zorder.rb +3 -0
- data/media/Beep.wav +0 -0
- data/media/Space.png +0 -0
- data/media/Star.png +0 -0
- data/media/Starfighter.bmp +0 -0
- data/media/brick.gif +0 -0
- data/media/cement.gif +0 -0
- data/media/crosshair.gif +0 -0
- data/media/dirt.gif +0 -0
- data/media/pellet.png +0 -0
- data/media/pellet.xcf +0 -0
- data/media/player.png +0 -0
- data/media/player.xcf +0 -0
- data/media/rock.png +0 -0
- data/media/rock.xcf +0 -0
- data/media/steel.gif +0 -0
- data/media/tele.gif +0 -0
- data/media/titanium.gif +0 -0
- data/media/unlikelium.gif +0 -0
- data/spec/client_engine_spec.rb +235 -0
- data/spec/game_space_spec.rb +347 -0
- metadata +246 -0
| @@ -0,0 +1,260 @@ | |
| 1 | 
            +
            ## Author: Greg Meyers
         | 
| 2 | 
            +
            ## License: Same as for Gosu (MIT)
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            require 'rubygems'
         | 
| 5 | 
            +
            require 'gosu'
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            require 'game_2d/client_connection'
         | 
| 8 | 
            +
            require 'game_2d/client_engine'
         | 
| 9 | 
            +
            require 'game_2d/game_space'
         | 
| 10 | 
            +
            require 'game_2d/entity'
         | 
| 11 | 
            +
            require 'game_2d/player'
         | 
| 12 | 
            +
            require 'game_2d/menu'
         | 
| 13 | 
            +
            require 'game_2d/zorder'
         | 
| 14 | 
            +
             | 
| 15 | 
            +
            SCREEN_WIDTH = 640  # in pixels
         | 
| 16 | 
            +
            SCREEN_HEIGHT = 480 # in pixels
         | 
| 17 | 
            +
             | 
| 18 | 
            +
            DEFAULT_PORT = 4321
         | 
| 19 | 
            +
             | 
| 20 | 
            +
            # The Gosu::Window is always the "environment" of our game
         | 
| 21 | 
            +
            # It also provides the pulse of our game
         | 
| 22 | 
            +
            class GameWindow < Gosu::Window
         | 
| 23 | 
            +
              attr_reader :animation, :font
         | 
| 24 | 
            +
              attr_accessor :player_id
         | 
| 25 | 
            +
             | 
| 26 | 
            +
              def initialize(player_name, hostname, port=DEFAULT_PORT, profile=false)
         | 
| 27 | 
            +
                @conn_update_total = @engine_update_total = 0.0
         | 
| 28 | 
            +
                @conn_update_count = @engine_update_count = 0
         | 
| 29 | 
            +
                @profile = profile
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                super(SCREEN_WIDTH, SCREEN_HEIGHT, false, 16)
         | 
| 32 | 
            +
                self.caption = "Ruby Gosu Game"
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                @pressed_buttons = []
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                @background_image = Gosu::Image.new(self, media("Space.png"), true)
         | 
| 37 | 
            +
                @animation = Hash.new do |h, k|
         | 
| 38 | 
            +
                  h[k] = Gosu::Image::load_tiles(
         | 
| 39 | 
            +
                    self, k, Entity::CELL_WIDTH_IN_PIXELS, Entity::CELL_WIDTH_IN_PIXELS, false)
         | 
| 40 | 
            +
                end
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                @cursor_anim = @animation[media("crosshair.gif")]
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                @beep = Gosu::Sample.new(self, media("Beep.wav")) # not used yet
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                @font = Gosu::Font.new(self, Gosu::default_font_name, 20)
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                # Local settings
         | 
| 49 | 
            +
                @local = {
         | 
| 50 | 
            +
                  :create_npc => {
         | 
| 51 | 
            +
                    :type => 'Entity::Block',
         | 
| 52 | 
            +
                    :hp   => 5,
         | 
| 53 | 
            +
                    :snap => false,
         | 
| 54 | 
            +
                  },
         | 
| 55 | 
            +
                }
         | 
| 56 | 
            +
                snap_text = lambda do |item|
         | 
| 57 | 
            +
                  @local[:create_npc][:snap] ? "Turn snap off" : "Turn snap on"
         | 
| 58 | 
            +
                end
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                object_type_submenus = [
         | 
| 61 | 
            +
                  ['Dirt',       'Entity::Block',    5],
         | 
| 62 | 
            +
                  ['Brick',      'Entity::Block',    10],
         | 
| 63 | 
            +
                  ['Cement',     'Entity::Block',    15],
         | 
| 64 | 
            +
                  ['Steel',      'Entity::Block',    20],
         | 
| 65 | 
            +
                  ['Unlikelium', 'Entity::Block',    25],
         | 
| 66 | 
            +
                  ['Titanium',   'Entity::Titanium', 0]
         | 
| 67 | 
            +
                ].collect do |type_name, class_name, hp|
         | 
| 68 | 
            +
                  MenuItem.new(type_name, self, @font) do |item|
         | 
| 69 | 
            +
                    @local[:create_npc][:type] = class_name
         | 
| 70 | 
            +
                    @local[:create_npc][:hp] = hp
         | 
| 71 | 
            +
                  end
         | 
| 72 | 
            +
                end
         | 
| 73 | 
            +
                object_type_menu = Menu.new('Object type', self, @font,
         | 
| 74 | 
            +
                  *object_type_submenus)
         | 
| 75 | 
            +
             | 
| 76 | 
            +
                object_creation_menu = Menu.new('Object creation', self, @font,
         | 
| 77 | 
            +
                  MenuItem.new('Object type', self, @font) { object_type_menu },
         | 
| 78 | 
            +
                  MenuItem.new(snap_text, self, @font) do
         | 
| 79 | 
            +
                    @local[:create_npc][:snap] = !@local[:create_npc][:snap]
         | 
| 80 | 
            +
                  end,
         | 
| 81 | 
            +
                  MenuItem.new('Save!', self, @font) { @conn.send_save }
         | 
| 82 | 
            +
                )
         | 
| 83 | 
            +
                main_menu = Menu.new('Main menu', self, @font,
         | 
| 84 | 
            +
                  MenuItem.new('Object creation', self, @font) { object_creation_menu },
         | 
| 85 | 
            +
                  MenuItem.new('Quit!', self, @font) { shutdown }
         | 
| 86 | 
            +
                )
         | 
| 87 | 
            +
                @menu = @top_menu = MenuItem.new('Click for menu', self, @font) { main_menu }
         | 
| 88 | 
            +
             | 
| 89 | 
            +
                # Connect to server and kick off handshaking
         | 
| 90 | 
            +
                # We will create our player object only after we've been accepted by the server
         | 
| 91 | 
            +
                # and told our starting position
         | 
| 92 | 
            +
                @conn = ClientConnection.new(hostname, port, self, player_name)
         | 
| 93 | 
            +
                @engine = @conn.engine = ClientEngine.new(self)
         | 
| 94 | 
            +
                @run_start = Time.now.to_f
         | 
| 95 | 
            +
                @update_count = 0
         | 
| 96 | 
            +
              end
         | 
| 97 | 
            +
             | 
| 98 | 
            +
              def media(filename)
         | 
| 99 | 
            +
                "#{File.dirname __FILE__}/../../media/#{filename}"
         | 
| 100 | 
            +
              end
         | 
| 101 | 
            +
             | 
| 102 | 
            +
              def space
         | 
| 103 | 
            +
                @engine.space
         | 
| 104 | 
            +
              end
         | 
| 105 | 
            +
             | 
| 106 | 
            +
              def player
         | 
| 107 | 
            +
                space[@player_id]
         | 
| 108 | 
            +
              end
         | 
| 109 | 
            +
             | 
| 110 | 
            +
              def update
         | 
| 111 | 
            +
                @update_count += 1
         | 
| 112 | 
            +
             | 
| 113 | 
            +
                # Handle any pending ENet events
         | 
| 114 | 
            +
                before_t = Time.now.to_f
         | 
| 115 | 
            +
                @conn.update
         | 
| 116 | 
            +
                if @profile
         | 
| 117 | 
            +
                  @conn_update_total += (Time.now.to_f - before_t)
         | 
| 118 | 
            +
                  @conn_update_count += 1
         | 
| 119 | 
            +
                  $stderr.puts "@conn.update() averages #{@conn_update_total / @conn_update_count} seconds each" if (@conn_update_count % 60) == 0
         | 
| 120 | 
            +
                end
         | 
| 121 | 
            +
                return unless @conn.online? && @engine
         | 
| 122 | 
            +
             | 
| 123 | 
            +
                before_t = Time.now.to_f
         | 
| 124 | 
            +
                @engine.update
         | 
| 125 | 
            +
                if @profile
         | 
| 126 | 
            +
                  @engine_update_total += (Time.now.to_f - before_t)
         | 
| 127 | 
            +
                  @engine_update_count += 1
         | 
| 128 | 
            +
                  $stderr.puts "@engine.update() averages #{@engine_update_total / @engine_update_count} seconds" if (@engine_update_count % 60) == 0
         | 
| 129 | 
            +
                end
         | 
| 130 | 
            +
             | 
| 131 | 
            +
                # Player at the keyboard queues up a command
         | 
| 132 | 
            +
                # @pressed_buttons is emptied by handle_input
         | 
| 133 | 
            +
                handle_input if @player_id
         | 
| 134 | 
            +
             | 
| 135 | 
            +
                $stderr.puts "Updates per second: #{@update_count / (Time.now.to_f - @run_start)}" if @profile
         | 
| 136 | 
            +
              end
         | 
| 137 | 
            +
             | 
| 138 | 
            +
              def draw
         | 
| 139 | 
            +
                @background_image.draw(0, 0, ZOrder::Background)
         | 
| 140 | 
            +
                return unless @player_id
         | 
| 141 | 
            +
                @camera_x, @camera_y = space.good_camera_position_for(player, SCREEN_WIDTH, SCREEN_HEIGHT)
         | 
| 142 | 
            +
                translate(-@camera_x, -@camera_y) do
         | 
| 143 | 
            +
                  (space.players + space.npcs).each {|entity| entity.draw(self) }
         | 
| 144 | 
            +
                end
         | 
| 145 | 
            +
             | 
| 146 | 
            +
                space.players.sort.each_with_index do |player, num|
         | 
| 147 | 
            +
                  @font.draw("#{player.player_name}: #{player.score}", 10, 10 * (num * 2 + 1), ZOrder::Text, 1.0, 1.0, Gosu::Color::YELLOW)
         | 
| 148 | 
            +
                end
         | 
| 149 | 
            +
             | 
| 150 | 
            +
                @menu.draw
         | 
| 151 | 
            +
             | 
| 152 | 
            +
                cursor_img = @cursor_anim[Gosu::milliseconds / 50 % @cursor_anim.size]
         | 
| 153 | 
            +
                cursor_img.draw(
         | 
| 154 | 
            +
                  mouse_x - cursor_img.width / 2.0,
         | 
| 155 | 
            +
                  mouse_y - cursor_img.height / 2.0,
         | 
| 156 | 
            +
                  ZOrder::Cursor,
         | 
| 157 | 
            +
                  1, 1, Gosu::Color::WHITE, :add)
         | 
| 158 | 
            +
              end
         | 
| 159 | 
            +
             | 
| 160 | 
            +
              def draw_box_at(x1, y1, x2, y2, c)
         | 
| 161 | 
            +
                draw_quad(x1, y1, c, x2, y1, c, x2, y2, c, x1, y2, c, ZOrder::Highlight)
         | 
| 162 | 
            +
              end
         | 
| 163 | 
            +
             | 
| 164 | 
            +
              def button_down(id)
         | 
| 165 | 
            +
                case id
         | 
| 166 | 
            +
                  when Gosu::KbP then @conn.send_ping
         | 
| 167 | 
            +
                  when Gosu::KbEscape then @menu = @top_menu
         | 
| 168 | 
            +
                  when Gosu::MsLeft then # left-click
         | 
| 169 | 
            +
                    if new_menu = @menu.handle_click
         | 
| 170 | 
            +
                      # If handle_click returned anything, the menu consumed the click
         | 
| 171 | 
            +
                      # If it returned a menu, that's the new one we display
         | 
| 172 | 
            +
                      @menu = (new_menu.respond_to?(:handle_click) ? new_menu : @top_menu)
         | 
| 173 | 
            +
                    else
         | 
| 174 | 
            +
                      send_fire
         | 
| 175 | 
            +
                    end
         | 
| 176 | 
            +
                  when Gosu::MsRight then # right-click
         | 
| 177 | 
            +
                    send_create_npc
         | 
| 178 | 
            +
                  else @pressed_buttons << id
         | 
| 179 | 
            +
                end
         | 
| 180 | 
            +
              end
         | 
| 181 | 
            +
             | 
| 182 | 
            +
              def send_fire
         | 
| 183 | 
            +
                return unless @player_id
         | 
| 184 | 
            +
                x, y = mouse_coords
         | 
| 185 | 
            +
                x_vel = (x - (player.x + Entity::WIDTH / 2)) / Entity::PIXEL_WIDTH
         | 
| 186 | 
            +
                y_vel = (y - (player.y + Entity::WIDTH / 2)) / Entity::PIXEL_WIDTH
         | 
| 187 | 
            +
                @conn.send_move :fire, :x_vel => x_vel, :y_vel => y_vel
         | 
| 188 | 
            +
              end
         | 
| 189 | 
            +
             | 
| 190 | 
            +
              # X/Y position of the mouse (center of the crosshairs), adjusted for camera
         | 
| 191 | 
            +
              def mouse_coords
         | 
| 192 | 
            +
                # For some reason, Gosu's mouse_x/mouse_y return Floats, so round it off
         | 
| 193 | 
            +
                [
         | 
| 194 | 
            +
                  (mouse_x.round + @camera_x) * Entity::PIXEL_WIDTH,
         | 
| 195 | 
            +
                  (mouse_y.round + @camera_y) * Entity::PIXEL_WIDTH
         | 
| 196 | 
            +
                ]
         | 
| 197 | 
            +
              end
         | 
| 198 | 
            +
             | 
| 199 | 
            +
              def send_create_npc
         | 
| 200 | 
            +
                x, y = mouse_coords
         | 
| 201 | 
            +
             | 
| 202 | 
            +
                if @local[:create_npc][:snap]
         | 
| 203 | 
            +
                  # When snap is on, we want the upper-left corner of the cell we clicked in
         | 
| 204 | 
            +
                  x = (x / Entity::WIDTH) * Entity::WIDTH
         | 
| 205 | 
            +
                  y = (y / Entity::HEIGHT) * Entity::HEIGHT
         | 
| 206 | 
            +
                else
         | 
| 207 | 
            +
                  # When snap is off, we want the click to be the new entity's center, not
         | 
| 208 | 
            +
                  # its upper-left corner
         | 
| 209 | 
            +
                  x -= Entity::WIDTH / 2
         | 
| 210 | 
            +
                  y -= Entity::HEIGHT / 2
         | 
| 211 | 
            +
                end
         | 
| 212 | 
            +
             | 
| 213 | 
            +
                @conn.send_create_npc(
         | 
| 214 | 
            +
                  :class => @local[:create_npc][:type],
         | 
| 215 | 
            +
                  :position => [x, y],
         | 
| 216 | 
            +
                  :velocity => [0, 0],
         | 
| 217 | 
            +
                  :angle => 0,
         | 
| 218 | 
            +
                  :moving => true,
         | 
| 219 | 
            +
                  :hp => @local[:create_npc][:hp]
         | 
| 220 | 
            +
                )
         | 
| 221 | 
            +
              end
         | 
| 222 | 
            +
             | 
| 223 | 
            +
              def shutdown
         | 
| 224 | 
            +
                @conn.disconnect
         | 
| 225 | 
            +
                close
         | 
| 226 | 
            +
              end
         | 
| 227 | 
            +
             | 
| 228 | 
            +
              # Dequeue an input event
         | 
| 229 | 
            +
              def handle_input
         | 
| 230 | 
            +
                return if player.falling?
         | 
| 231 | 
            +
                move = move_for_keypress
         | 
| 232 | 
            +
                @conn.send_move move # also creates a delta in the engine
         | 
| 233 | 
            +
              end
         | 
| 234 | 
            +
             | 
| 235 | 
            +
              # Check keyboard, return a motion symbol or nil
         | 
| 236 | 
            +
              #
         | 
| 237 | 
            +
              #
         | 
| 238 | 
            +
              def move_for_keypress
         | 
| 239 | 
            +
                # Generated once for each keypress
         | 
| 240 | 
            +
                until @pressed_buttons.empty?
         | 
| 241 | 
            +
                  button = @pressed_buttons.shift
         | 
| 242 | 
            +
                  case button
         | 
| 243 | 
            +
                    when Gosu::KbUp, Gosu::KbW
         | 
| 244 | 
            +
                      return (player.building?) ? :rise_up : :flip
         | 
| 245 | 
            +
                  end
         | 
| 246 | 
            +
                end
         | 
| 247 | 
            +
             | 
| 248 | 
            +
                # Continuously-generated when key held down
         | 
| 249 | 
            +
                case
         | 
| 250 | 
            +
                  when button_down?(Gosu::KbLeft), button_down?(Gosu::KbA)
         | 
| 251 | 
            +
                    :slide_left
         | 
| 252 | 
            +
                  when button_down?(Gosu::KbRight), button_down?(Gosu::KbD)
         | 
| 253 | 
            +
                    :slide_right
         | 
| 254 | 
            +
                  when button_down?(Gosu::KbRightControl), button_down?(Gosu::KbLeftControl)
         | 
| 255 | 
            +
                    :brake
         | 
| 256 | 
            +
                  when button_down?(Gosu::KbDown), button_down?(Gosu::KbS)
         | 
| 257 | 
            +
                    :build
         | 
| 258 | 
            +
                end
         | 
| 259 | 
            +
              end
         | 
| 260 | 
            +
            end
         | 
    
        data/lib/game_2d/hash.rb
    ADDED
    
    
    
        data/lib/game_2d/menu.rb
    ADDED
    
    | @@ -0,0 +1,82 @@ | |
| 1 | 
            +
            require 'gosu'
         | 
| 2 | 
            +
            require 'game_2d/zorder'
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            class Menu
         | 
| 5 | 
            +
              def initialize(name, window, font, *choices)
         | 
| 6 | 
            +
                @name, @window, @font, @choices = name, window, font, choices
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                @main_color, @select_color = Gosu::Color::YELLOW, Gosu::Color::CYAN
         | 
| 9 | 
            +
                @right = window.width - 1
         | 
| 10 | 
            +
                @choices.each_with_index do |choice, num|
         | 
| 11 | 
            +
                  choice.x = @right
         | 
| 12 | 
            +
                  choice.y = (num + 2) * 20
         | 
| 13 | 
            +
                end
         | 
| 14 | 
            +
              end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
              def draw
         | 
| 17 | 
            +
                str = to_s
         | 
| 18 | 
            +
                @font.draw_rel(str, @window.width - 1, 0, ZOrder::Text, 1.0, 0.0, 1.0, 1.0,
         | 
| 19 | 
            +
                  @main_color)
         | 
| 20 | 
            +
                x1, x2, y, c = @right - @font.text_width(str), @right, 20, @main_color
         | 
| 21 | 
            +
                @window.draw_box_at(x1, y, x2, y+1, @main_color)
         | 
| 22 | 
            +
                @choices.each(&:draw)
         | 
| 23 | 
            +
              end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
              # Returns a true value if it handled the click
         | 
| 26 | 
            +
              # May return a Menu or MenuItem to be set as the new menu to display
         | 
| 27 | 
            +
              # May return simply 'true' if we should redisplay the top-level menu
         | 
| 28 | 
            +
              def handle_click
         | 
| 29 | 
            +
                @choices.collect(&:handle_click).compact.first
         | 
| 30 | 
            +
              end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
              def to_s
         | 
| 33 | 
            +
                @name.respond_to?(:call) ? @name.call(self) : @name.to_s
         | 
| 34 | 
            +
              end
         | 
| 35 | 
            +
            end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
            class MenuItem
         | 
| 38 | 
            +
              attr_accessor :x, :y, :name
         | 
| 39 | 
            +
              def initialize(name, window, font, &action)
         | 
| 40 | 
            +
                @name, @window, @font, @action = name, window, font, action
         | 
| 41 | 
            +
                @main_color, @select_color, @highlight_color =
         | 
| 42 | 
            +
                  Gosu::Color::YELLOW, Gosu::Color::BLACK, Gosu::Color::CYAN
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                # Default position: Upper-right corner
         | 
| 45 | 
            +
                @x, @y = @window.width - 1, 0
         | 
| 46 | 
            +
              end
         | 
| 47 | 
            +
             | 
| 48 | 
            +
              def mouse_over?
         | 
| 49 | 
            +
                x, y = @window.mouse_x, @window.mouse_y
         | 
| 50 | 
            +
                (y >= top) && (y < bottom) && (x > left)
         | 
| 51 | 
            +
              end
         | 
| 52 | 
            +
             | 
| 53 | 
            +
              def left; @x - @font.text_width(to_s); end
         | 
| 54 | 
            +
              def right; @x; end
         | 
| 55 | 
            +
              def top; @y; end
         | 
| 56 | 
            +
              def bottom; @y + 20; end
         | 
| 57 | 
            +
             | 
| 58 | 
            +
              def draw
         | 
| 59 | 
            +
                selected = mouse_over?
         | 
| 60 | 
            +
                color = choose_color(selected)
         | 
| 61 | 
            +
                @font.draw_rel(to_s, @x, @y, ZOrder::Text, 1.0, 0.0, 1.0, 1.0, color)
         | 
| 62 | 
            +
                if selected
         | 
| 63 | 
            +
                  @window.draw_box_at(left, top, right, bottom, @highlight_color)
         | 
| 64 | 
            +
                end
         | 
| 65 | 
            +
              end
         | 
| 66 | 
            +
             | 
| 67 | 
            +
              def choose_color(selected)
         | 
| 68 | 
            +
                selected ? @select_color : @main_color
         | 
| 69 | 
            +
              end
         | 
| 70 | 
            +
             | 
| 71 | 
            +
              # Returns a true value if it handled the click
         | 
| 72 | 
            +
              # May return a Menu or MenuItem to be set as the new menu to display
         | 
| 73 | 
            +
              # May return simply 'true' if we should redisplay the top-level menu
         | 
| 74 | 
            +
              def handle_click
         | 
| 75 | 
            +
                return unless mouse_over?
         | 
| 76 | 
            +
                @action.call(self) || true
         | 
| 77 | 
            +
              end
         | 
| 78 | 
            +
             | 
| 79 | 
            +
              def to_s
         | 
| 80 | 
            +
                @name.respond_to?(:call) ? @name.call(self) : @name.to_s
         | 
| 81 | 
            +
              end
         | 
| 82 | 
            +
            end
         | 
| @@ -0,0 +1,77 @@ | |
| 1 | 
            +
            require 'game_2d/complex_move'
         | 
| 2 | 
            +
            require 'game_2d/entity_constants'
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            module Move
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            class RiseUp < ComplexMove
         | 
| 7 | 
            +
              include EntityConstants
         | 
| 8 | 
            +
             | 
| 9 | 
            +
              # Valid stages: :center, :rise, :reset
         | 
| 10 | 
            +
              # Distance is how much further we need to go
         | 
| 11 | 
            +
              # (in pixels) in stage :rise
         | 
| 12 | 
            +
              attr_accessor :stage, :distance
         | 
| 13 | 
            +
              def initialize(actor=nil)
         | 
| 14 | 
            +
                super
         | 
| 15 | 
            +
                @stage = :center
         | 
| 16 | 
            +
              end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
              def on_completion(actor)
         | 
| 19 | 
            +
                actor.instance_exec { @x_vel = @y_vel = 0 }
         | 
| 20 | 
            +
              end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
              def update(actor)
         | 
| 23 | 
            +
                # It's convenient to set 'self' to the Player
         | 
| 24 | 
            +
                # object, here
         | 
| 25 | 
            +
                actor.instance_exec(self) do |cm|
         | 
| 26 | 
            +
                  # Abort if the build_block gets destroyed
         | 
| 27 | 
            +
                  blok = build_block
         | 
| 28 | 
            +
                  return false unless blok
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                  start_x, start_y = blok.x, blok.y
         | 
| 31 | 
            +
                  case cm.stage
         | 
| 32 | 
            +
                  when :center, :reset
         | 
| 33 | 
            +
                    if x == start_x && y == start_y
         | 
| 34 | 
            +
                      # If we're in reset, we're all done
         | 
| 35 | 
            +
                      return false if cm.stage == :reset
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                      # Establish our velocity for :rise
         | 
| 38 | 
            +
                      cm.stage = :rise
         | 
| 39 | 
            +
                      @x_vel, @y_vel = angle_to_vector(a, PIXEL_WIDTH)
         | 
| 40 | 
            +
                      cm.distance = CELL_WIDTH_IN_PIXELS
         | 
| 41 | 
            +
                      return cm.update(self)
         | 
| 42 | 
            +
                    end
         | 
| 43 | 
            +
                    @x_vel = [[start_x - x, -PIXEL_WIDTH].max, PIXEL_WIDTH].min
         | 
| 44 | 
            +
                    @y_vel = [[start_y - y, -PIXEL_WIDTH].max, PIXEL_WIDTH].min
         | 
| 45 | 
            +
                    # move returns false: it failed somehow
         | 
| 46 | 
            +
                    return move
         | 
| 47 | 
            +
                  when :rise
         | 
| 48 | 
            +
                    # Success
         | 
| 49 | 
            +
                    return false if cm.distance.zero?
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                    cm.distance -= 1
         | 
| 52 | 
            +
                    # move fails? Go to :reset
         | 
| 53 | 
            +
                    move || (cm.stage = :reset)
         | 
| 54 | 
            +
                    return true
         | 
| 55 | 
            +
                  end
         | 
| 56 | 
            +
                end
         | 
| 57 | 
            +
              end
         | 
| 58 | 
            +
             | 
| 59 | 
            +
              def all_state
         | 
| 60 | 
            +
                super.push @stage, @distance
         | 
| 61 | 
            +
              end
         | 
| 62 | 
            +
              def as_json
         | 
| 63 | 
            +
                super.merge! :stage => @stage,
         | 
| 64 | 
            +
                  :distance => @distance
         | 
| 65 | 
            +
              end
         | 
| 66 | 
            +
              def update_from_json(json)
         | 
| 67 | 
            +
                self.stage = json[:stage].to_sym
         | 
| 68 | 
            +
                self.distance = json[:distance]
         | 
| 69 | 
            +
                super
         | 
| 70 | 
            +
              end
         | 
| 71 | 
            +
              def to_s
         | 
| 72 | 
            +
                "RiseUp[#{stage}, #{distance} to go]"
         | 
| 73 | 
            +
              end
         | 
| 74 | 
            +
             | 
| 75 | 
            +
            end
         | 
| 76 | 
            +
             | 
| 77 | 
            +
            end
         | 
| @@ -0,0 +1,251 @@ | |
| 1 | 
            +
            require 'facets/kernel/try'
         | 
| 2 | 
            +
            require 'gosu'
         | 
| 3 | 
            +
            require 'game_2d/entity'
         | 
| 4 | 
            +
            require 'game_2d/entity/pellet'
         | 
| 5 | 
            +
            require 'game_2d/entity/block'
         | 
| 6 | 
            +
            require 'game_2d/move/rise_up'
         | 
| 7 | 
            +
            require 'game_2d/zorder'
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            # The base Player class representing what all Players have in common
         | 
| 10 | 
            +
            # Moves can be enqueued by calling add_move
         | 
| 11 | 
            +
            # Calling update() causes a move to be dequeued and executed, applying forces
         | 
| 12 | 
            +
            # to the game object
         | 
| 13 | 
            +
            #
         | 
| 14 | 
            +
            # The server instantiates this class to represent each connected player
         | 
| 15 | 
            +
            class Player < Entity
         | 
| 16 | 
            +
              include Comparable
         | 
| 17 | 
            +
             | 
| 18 | 
            +
              # Game ticks it takes before a block's HP is raised by 1
         | 
| 19 | 
            +
              BUILD_TIME = 7
         | 
| 20 | 
            +
             | 
| 21 | 
            +
              # Amount to decelerate each tick when braking
         | 
| 22 | 
            +
              BRAKE_SPEED = 4
         | 
| 23 | 
            +
             | 
| 24 | 
            +
              attr_accessor :player_name, :score
         | 
| 25 | 
            +
              attr_reader :build_block_id
         | 
| 26 | 
            +
             | 
| 27 | 
            +
              def initialize(player_name = "<unknown>")
         | 
| 28 | 
            +
                super
         | 
| 29 | 
            +
                @player_name = player_name
         | 
| 30 | 
            +
                @score = 0
         | 
| 31 | 
            +
                @moves = []
         | 
| 32 | 
            +
                @current_move = nil
         | 
| 33 | 
            +
                @falling = false
         | 
| 34 | 
            +
                @build_block_id = nil
         | 
| 35 | 
            +
                @build_level = 0
         | 
| 36 | 
            +
                @complex_move = nil
         | 
| 37 | 
            +
              end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
              def sleep_now?; false; end
         | 
| 40 | 
            +
             | 
| 41 | 
            +
              def falling?; @falling; end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
              def build_block_id=(new_id)
         | 
| 44 | 
            +
                @build_block_id = new_id.try(:to_sym)
         | 
| 45 | 
            +
              end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
              def building?; @build_block_id; end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
              def build_block
         | 
| 50 | 
            +
                return nil unless building?
         | 
| 51 | 
            +
                fail "Can't look up build_block when not in a space" unless @space
         | 
| 52 | 
            +
                @space[@build_block_id] or fail "Don't have build_block #{@build_block_id}"
         | 
| 53 | 
            +
              end
         | 
| 54 | 
            +
             | 
| 55 | 
            +
              def destroy!
         | 
| 56 | 
            +
                build_block.owner_id = nil if building?
         | 
| 57 | 
            +
              end
         | 
| 58 | 
            +
             | 
| 59 | 
            +
              # Pellets don't hit the originating player
         | 
| 60 | 
            +
              def transparent_to_me?(other)
         | 
| 61 | 
            +
                super ||
         | 
| 62 | 
            +
                (other == build_block) ||
         | 
| 63 | 
            +
                (other.is_a?(Pellet) && other.owner == self)
         | 
| 64 | 
            +
              end
         | 
| 65 | 
            +
             | 
| 66 | 
            +
              def update
         | 
| 67 | 
            +
                fail "No space set for #{self}" unless @space
         | 
| 68 | 
            +
                check_for_disown_block
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                if @complex_move
         | 
| 71 | 
            +
                  # returns true if more work to do
         | 
| 72 | 
            +
                  return if @complex_move.update(self)
         | 
| 73 | 
            +
                  @complex_move.on_completion(self)
         | 
| 74 | 
            +
                  @complex_move = nil
         | 
| 75 | 
            +
                end
         | 
| 76 | 
            +
             | 
| 77 | 
            +
                underfoot = next_to(self.a + 180)
         | 
| 78 | 
            +
                if @falling = underfoot.empty?
         | 
| 79 | 
            +
                  self.a = 0
         | 
| 80 | 
            +
                  accelerate(0, 1)
         | 
| 81 | 
            +
                end
         | 
| 82 | 
            +
             | 
| 83 | 
            +
                args = @moves.shift
         | 
| 84 | 
            +
                case (current_move = args.delete(:move).to_sym)
         | 
| 85 | 
            +
                  when :slide_left, :slide_right, :brake, :flip, :build, :rise_up
         | 
| 86 | 
            +
                    send current_move unless @falling
         | 
| 87 | 
            +
                  when :fire
         | 
| 88 | 
            +
                    fire args[:x_vel], args[:y_vel]
         | 
| 89 | 
            +
                  else
         | 
| 90 | 
            +
                    puts "Invalid move for #{self}: #{current_move}, #{args.inspect}"
         | 
| 91 | 
            +
                end if args
         | 
| 92 | 
            +
             | 
| 93 | 
            +
                # Only go around corner if sitting on exactly one object
         | 
| 94 | 
            +
                if underfoot.size == 1
         | 
| 95 | 
            +
                  other = underfoot.first
         | 
| 96 | 
            +
                  # Figure out where corner is and whether we're about to reach or pass it
         | 
| 97 | 
            +
                  corner, distance, overshoot, turn = going_past_entity(other.x, other.y)
         | 
| 98 | 
            +
                  if corner
         | 
| 99 | 
            +
                    original_speed = @x_vel.abs + @y_vel.abs
         | 
| 100 | 
            +
                    original_dir = vector_to_angle
         | 
| 101 | 
            +
                    new_dir = original_dir + turn
         | 
| 102 | 
            +
             | 
| 103 | 
            +
                    # Make sure nothing occupies any space we're about to move through
         | 
| 104 | 
            +
                    if opaque(
         | 
| 105 | 
            +
                      @space.entities_overlapping(*corner) + next_to(new_dir, *corner)
         | 
| 106 | 
            +
                    ).empty?
         | 
| 107 | 
            +
                      # Move to the corner
         | 
| 108 | 
            +
                      self.x_vel, self.y_vel = angle_to_vector(original_dir, distance)
         | 
| 109 | 
            +
                      move
         | 
| 110 | 
            +
             | 
| 111 | 
            +
                      # Turn and apply remaining velocity
         | 
| 112 | 
            +
                      # Make sure we move at least one subpixel so we don't sit exactly at
         | 
| 113 | 
            +
                      # the corner, and fall
         | 
| 114 | 
            +
                      self.a += turn
         | 
| 115 | 
            +
                      overshoot = 1 if overshoot.zero?
         | 
| 116 | 
            +
                      self.x_vel, self.y_vel = angle_to_vector(new_dir, overshoot)
         | 
| 117 | 
            +
                      move
         | 
| 118 | 
            +
             | 
| 119 | 
            +
                      self.x_vel, self.y_vel = angle_to_vector(new_dir, original_speed)
         | 
| 120 | 
            +
                    else
         | 
| 121 | 
            +
                      # Something's in the way -- possibly in front of us, or possibly
         | 
| 122 | 
            +
                      # around the corner
         | 
| 123 | 
            +
                      move
         | 
| 124 | 
            +
                    end
         | 
| 125 | 
            +
                  else
         | 
| 126 | 
            +
                    # Not yet reaching the corner -- or making a diagonal motion, for which
         | 
| 127 | 
            +
                    # we can't support going around the corner
         | 
| 128 | 
            +
                    move
         | 
| 129 | 
            +
                  end
         | 
| 130 | 
            +
                else
         | 
| 131 | 
            +
                  # Straddling two objects, or falling
         | 
| 132 | 
            +
                  move
         | 
| 133 | 
            +
                end
         | 
| 134 | 
            +
             | 
| 135 | 
            +
                # Check again whether we've moved off of a block
         | 
| 136 | 
            +
                # we were building
         | 
| 137 | 
            +
                check_for_disown_block
         | 
| 138 | 
            +
              end
         | 
| 139 | 
            +
             | 
| 140 | 
            +
              def slide_left; slide(self.a - 90); end
         | 
| 141 | 
            +
              def slide_right; slide(self.a + 90); end
         | 
| 142 | 
            +
             | 
| 143 | 
            +
              def slide(dir)
         | 
| 144 | 
            +
                if opaque(next_to(dir)).empty?
         | 
| 145 | 
            +
                  accelerate(*angle_to_vector(dir))
         | 
| 146 | 
            +
                else
         | 
| 147 | 
            +
                  self.a = dir + 180
         | 
| 148 | 
            +
                end
         | 
| 149 | 
            +
              end
         | 
| 150 | 
            +
             | 
| 151 | 
            +
              def brake
         | 
| 152 | 
            +
                if @x_vel.zero?
         | 
| 153 | 
            +
                  self.y_vel = brake_velocity(@y_vel)
         | 
| 154 | 
            +
                else
         | 
| 155 | 
            +
                  self.x_vel = brake_velocity(@x_vel)
         | 
| 156 | 
            +
                end
         | 
| 157 | 
            +
              end
         | 
| 158 | 
            +
             | 
| 159 | 
            +
              def brake_velocity(v)
         | 
| 160 | 
            +
                return 0 if v.abs < BRAKE_SPEED
         | 
| 161 | 
            +
                sign = v <=> 0
         | 
| 162 | 
            +
                sign * (v.abs - BRAKE_SPEED)
         | 
| 163 | 
            +
              end
         | 
| 164 | 
            +
             | 
| 165 | 
            +
              def flip
         | 
| 166 | 
            +
                self.a += 180
         | 
| 167 | 
            +
              end
         | 
| 168 | 
            +
             | 
| 169 | 
            +
              # Create the actual pellet
         | 
| 170 | 
            +
              def fire(x_vel, y_vel)
         | 
| 171 | 
            +
                pellet = Entity::Pellet.new(@x, @y, 0, x_vel, y_vel)
         | 
| 172 | 
            +
                pellet.owner = self
         | 
| 173 | 
            +
                @space << pellet
         | 
| 174 | 
            +
              end
         | 
| 175 | 
            +
             | 
| 176 | 
            +
              # Create the actual block
         | 
| 177 | 
            +
              def build
         | 
| 178 | 
            +
                if building?
         | 
| 179 | 
            +
                  @build_level += 1
         | 
| 180 | 
            +
                  if @build_level >= BUILD_TIME
         | 
| 181 | 
            +
                    @build_level = 0
         | 
| 182 | 
            +
                    build_block.hp += 1
         | 
| 183 | 
            +
                  end
         | 
| 184 | 
            +
                else
         | 
| 185 | 
            +
                  bb = Entity::Block.new(@x, @y)
         | 
| 186 | 
            +
                  bb.owner_id = registry_id
         | 
| 187 | 
            +
                  bb.hp = 1
         | 
| 188 | 
            +
                  @space << bb # generates an ID
         | 
| 189 | 
            +
                  @build_block_id = bb.registry_id
         | 
| 190 | 
            +
                  @build_level = 0
         | 
| 191 | 
            +
                end
         | 
| 192 | 
            +
              end
         | 
| 193 | 
            +
             | 
| 194 | 
            +
              def disown_block; $stderr.puts "#{self} disowning #{build_block}"; @build_block_id, @build_level = nil, 0; end
         | 
| 195 | 
            +
             | 
| 196 | 
            +
              def check_for_disown_block
         | 
| 197 | 
            +
                return unless building?
         | 
| 198 | 
            +
                return if @space.entities_overlapping(@x, @y).include?(build_block)
         | 
| 199 | 
            +
                build_block.owner_id = nil
         | 
| 200 | 
            +
                build_block.wake!
         | 
| 201 | 
            +
                disown_block
         | 
| 202 | 
            +
              end
         | 
| 203 | 
            +
             | 
| 204 | 
            +
              def rise_up
         | 
| 205 | 
            +
                @complex_move = Move::RiseUp.new(self)
         | 
| 206 | 
            +
              end
         | 
| 207 | 
            +
             | 
| 208 | 
            +
              # Accepts a hash, with a key :move => move_type
         | 
| 209 | 
            +
              def add_move(new_move)
         | 
| 210 | 
            +
                return unless new_move
         | 
| 211 | 
            +
                @moves << new_move
         | 
| 212 | 
            +
              end
         | 
| 213 | 
            +
             | 
| 214 | 
            +
              def to_s
         | 
| 215 | 
            +
                "#{player_name} (#{registry_id_safe}) at #{x}x#{y}"
         | 
| 216 | 
            +
              end
         | 
| 217 | 
            +
             | 
| 218 | 
            +
              def all_state
         | 
| 219 | 
            +
                super.unshift(player_name).push(
         | 
| 220 | 
            +
                  score, build_block_id, @complex_move)
         | 
| 221 | 
            +
              end
         | 
| 222 | 
            +
             | 
| 223 | 
            +
              def as_json
         | 
| 224 | 
            +
                super.merge!(
         | 
| 225 | 
            +
                  :player_name => player_name,
         | 
| 226 | 
            +
                  :score => score,
         | 
| 227 | 
            +
                  :build_block => @build_block_id,
         | 
| 228 | 
            +
                  :complex_move => @complex_move.as_json
         | 
| 229 | 
            +
                )
         | 
| 230 | 
            +
              end
         | 
| 231 | 
            +
             | 
| 232 | 
            +
              def update_from_json(json)
         | 
| 233 | 
            +
                @player_name = json[:player_name]
         | 
| 234 | 
            +
                @score = json[:score]
         | 
| 235 | 
            +
                @build_block_id = json[:build_block].try(:to_sym)
         | 
| 236 | 
            +
                @complex_move = Serializable.from_json(json[:complex_move])
         | 
| 237 | 
            +
                super
         | 
| 238 | 
            +
              end
         | 
| 239 | 
            +
             | 
| 240 | 
            +
              def image_filename; "player.png"; end
         | 
| 241 | 
            +
             | 
| 242 | 
            +
              def draw_zorder; ZOrder::Player end
         | 
| 243 | 
            +
             | 
| 244 | 
            +
              def draw(window)
         | 
| 245 | 
            +
                super
         | 
| 246 | 
            +
                window.font.draw_rel(player_name,
         | 
| 247 | 
            +
                  pixel_x + CELL_WIDTH_IN_PIXELS / 2, pixel_y, ZOrder::Text,
         | 
| 248 | 
            +
                  0.5, 1.0, # Centered X; above Y
         | 
| 249 | 
            +
                  1.0, 1.0, Gosu::Color::YELLOW)
         | 
| 250 | 
            +
              end
         | 
| 251 | 
            +
            end
         |