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.
Files changed (53) hide show
  1. data/.gitignore +15 -0
  2. data/Gemfile +4 -0
  3. data/LICENSE.txt +22 -0
  4. data/README.md +84 -0
  5. data/Rakefile +1 -0
  6. data/bin/game_2d_client.rb +17 -0
  7. data/bin/game_2d_server.rb +21 -0
  8. data/game_2d.gemspec +32 -0
  9. data/lib/game_2d/client_connection.rb +127 -0
  10. data/lib/game_2d/client_engine.rb +227 -0
  11. data/lib/game_2d/complex_move.rb +45 -0
  12. data/lib/game_2d/entity.rb +371 -0
  13. data/lib/game_2d/entity/block.rb +73 -0
  14. data/lib/game_2d/entity/owned_entity.rb +29 -0
  15. data/lib/game_2d/entity/pellet.rb +27 -0
  16. data/lib/game_2d/entity/titanium.rb +11 -0
  17. data/lib/game_2d/entity_constants.rb +14 -0
  18. data/lib/game_2d/game.rb +213 -0
  19. data/lib/game_2d/game_space.rb +462 -0
  20. data/lib/game_2d/game_window.rb +260 -0
  21. data/lib/game_2d/hash.rb +11 -0
  22. data/lib/game_2d/menu.rb +82 -0
  23. data/lib/game_2d/move/rise_up.rb +77 -0
  24. data/lib/game_2d/player.rb +251 -0
  25. data/lib/game_2d/registerable.rb +25 -0
  26. data/lib/game_2d/serializable.rb +69 -0
  27. data/lib/game_2d/server_connection.rb +104 -0
  28. data/lib/game_2d/server_port.rb +74 -0
  29. data/lib/game_2d/storage.rb +42 -0
  30. data/lib/game_2d/version.rb +3 -0
  31. data/lib/game_2d/wall.rb +21 -0
  32. data/lib/game_2d/zorder.rb +3 -0
  33. data/media/Beep.wav +0 -0
  34. data/media/Space.png +0 -0
  35. data/media/Star.png +0 -0
  36. data/media/Starfighter.bmp +0 -0
  37. data/media/brick.gif +0 -0
  38. data/media/cement.gif +0 -0
  39. data/media/crosshair.gif +0 -0
  40. data/media/dirt.gif +0 -0
  41. data/media/pellet.png +0 -0
  42. data/media/pellet.xcf +0 -0
  43. data/media/player.png +0 -0
  44. data/media/player.xcf +0 -0
  45. data/media/rock.png +0 -0
  46. data/media/rock.xcf +0 -0
  47. data/media/steel.gif +0 -0
  48. data/media/tele.gif +0 -0
  49. data/media/titanium.gif +0 -0
  50. data/media/unlikelium.gif +0 -0
  51. data/spec/client_engine_spec.rb +235 -0
  52. data/spec/game_space_spec.rb +347 -0
  53. 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
@@ -0,0 +1,11 @@
1
+ require 'facets/array/recurse'
2
+ require 'facets/hash/recurse'
3
+ require 'facets/hash/symbolize_keys'
4
+
5
+ class Hash
6
+ def fix_keys
7
+ recurse(Hash, Array) do |x|
8
+ x.is_a?(Hash) ? x.symbolize_keys : x
9
+ end
10
+ end
11
+ end
@@ -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