game_2d 0.0.2 → 0.0.3

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.
@@ -0,0 +1,32 @@
1
+ require 'game_2d/entity'
2
+
3
+ class Entity
4
+
5
+ class Hole < Entity
6
+ def should_fall?; false; end
7
+
8
+ def apply_gravity_to?(entity)
9
+ distance = space.distance_between(cx, cy, entity.cx, entity.cy)
10
+ entity.harmed_by(self, (400 - distance).ceil) if distance < 400
11
+ force = 10000000.0 / (distance**2)
12
+ return true if force > 200.0
13
+ return false if force < 1.0
14
+ # We could use trig here -- but we have a shortcut.
15
+ # We know the X/Y proportions of the force must be
16
+ # the same as the X/Y proportions of the distance.
17
+ delta_x = cx - entity.cx
18
+ delta_y = cy - entity.cy
19
+ force_x = force * (delta_x / distance)
20
+ force_y = force * (delta_y / distance)
21
+ entity.accelerate(force_x.truncate, force_y.truncate)
22
+ true
23
+ end
24
+
25
+ def update; end
26
+
27
+ def image_filename; "hole.png"; end
28
+
29
+ def draw_zorder; ZOrder::Teleporter end
30
+ end
31
+
32
+ end
@@ -7,7 +7,7 @@ class Pellet < OwnedEntity
7
7
  def should_fall?; true end
8
8
  def sleep_now?; false end
9
9
 
10
- def i_hit(others)
10
+ def i_hit(others, velocity)
11
11
  puts "#{self}: hit #{others.inspect}. That's all for me."
12
12
  others.each {|other| other.harmed_by(self)}
13
13
  @space.doom(self)
@@ -0,0 +1,121 @@
1
+ require 'game_2d/entity/owned_entity'
2
+
3
+ class Entity
4
+
5
+ # Slime is like a lemming(tm): it either falls, or it walks left
6
+ # or right until forced to reverse direction
7
+ #
8
+ # As it comes into contact with other entities, it gradually
9
+ # increments its slime_count, until it maxes out. Then it harms
10
+ # whatever it just touched, and resets the count
11
+ class Slime < Entity
12
+ MAX_HP = 8
13
+ MAX_SPEED = 18
14
+ SLEEP_AMOUNT = 180
15
+ MAX_SLIME_COUNT = 100
16
+
17
+ attr_reader :hp, :sleep_count, :slime_count
18
+
19
+ def initialize
20
+ super
21
+ self.a = 270
22
+ @hp, @sleep_count, @slime_count = MAX_HP, 0, 0
23
+ end
24
+
25
+ def hp=(p); @hp = [[p, MAX_HP].min, 0].max; end
26
+
27
+ def all_state; super.push(hp, sleep_count, slime_count); end
28
+ def as_json
29
+ super.merge(
30
+ :hp => hp,
31
+ :sleep_count => @sleep_count,
32
+ :slime_count => @slime_count
33
+ )
34
+ end
35
+
36
+ def update_from_json(json)
37
+ self.hp = json[:hp] if json[:hp]
38
+ @sleep_count = json[:sleep_count] if json[:sleep_count]
39
+ @slime_count = json[:slime_count] if json[:slime_count]
40
+ super
41
+ end
42
+
43
+ def should_fall?; empty_underneath?; end
44
+
45
+ def trapped?; !(empty_on_left? || empty_on_right?); end
46
+
47
+ def sleep_now?; false; end
48
+
49
+ def update
50
+ if should_fall?
51
+ self.x_vel = @sleep_count = 0
52
+ space.fall(self)
53
+ move
54
+ elsif @sleep_count.zero?
55
+ slime_them(beneath, 1)
56
+ if trapped?
57
+ self.a += 180
58
+ slime_them((a == 270) ? on_left : on_right, 1)
59
+ @sleep_count = SLEEP_AMOUNT
60
+ else
61
+ self.y_vel = 0
62
+ accelerate((a == 270) ? -1 : 1, nil, MAX_SPEED)
63
+ advance
64
+ end
65
+ else
66
+ @sleep_count -= 1
67
+ end
68
+ end
69
+
70
+ def advance
71
+ blocks_underfoot = beneath
72
+ if blocks_underfoot.size == 1
73
+ # Slide around if we're at the corner; otherwise, move normally
74
+ # Don't allow slide_around() to adjust our angle
75
+ slide_around(blocks_underfoot.first, false) or move or turn_around
76
+ else
77
+ # Straddling two objects, or falling
78
+ move or turn_around
79
+ end
80
+ end
81
+
82
+ def turn_around; self.a += 180; end
83
+
84
+ def i_hit(others, velocity)
85
+ slime_them(others, velocity)
86
+ end
87
+
88
+ def slime_them(others, increment)
89
+ @slime_count += increment
90
+ if @slime_count > MAX_SLIME_COUNT
91
+ @slime_count -= MAX_SLIME_COUNT
92
+ others.each {|o| o.harmed_by(self) unless o.is_a? Slime}
93
+ end
94
+ end
95
+
96
+ def harmed_by(other, damage=1)
97
+ self.hp -= damage
98
+ @space.doom(self) if hp <= 0
99
+ end
100
+
101
+ def image_filename; "slime.png"; end
102
+
103
+ def draw(window)
104
+ img = draw_image(draw_animation(window))
105
+
106
+ # Default image faces left
107
+ # We don't rotate the slime; we just flip it horizontally
108
+ img.draw(
109
+ self.pixel_x + (a == 90 ? CELL_WIDTH_IN_PIXELS : 0), self.pixel_y, draw_zorder,
110
+ (a == 90 ? -1 : 1) # X scaling factor
111
+ )
112
+ # 0.5, 0.5, # rotate around the center
113
+ # 1, 1, # scaling factor
114
+ # @color, # modify color
115
+ # :add) # draw additively
116
+ end
117
+
118
+ def to_s; "#{super} (#{@hp} HP)"; end
119
+ end
120
+
121
+ end
@@ -6,9 +6,11 @@ class Entity
6
6
  class Teleporter < Entity
7
7
  def should_fall?; false; end
8
8
 
9
+ def teleportable?; false; end
10
+
9
11
  def update
10
12
  space.entities_overlapping(x, y).each do |overlap|
11
- next if overlap == self
13
+ next unless overlap.teleportable?
12
14
  next if (overlap.x - x).abs > WIDTH/2
13
15
  next if (overlap.y - y).abs > HEIGHT/2
14
16
  dest = space.possessions(self)
@@ -20,15 +22,15 @@ class Teleporter < Entity
20
22
  overlap.wake!
21
23
  end
22
24
  when 0 then
23
- $stderr.puts "#{self}: No destination"
25
+ warn "#{self}: No destination"
24
26
  else
25
- $stderr.puts "#{self}: Multiple destinations: #{dest.inspect}"
27
+ warn "#{self}: Multiple destinations: #{dest.inspect}"
26
28
  end
27
29
  end
28
30
  end
29
31
 
30
32
  def destroy!
31
- # destroy destination
33
+ space.possessions(self).each {|p| space.doom p}
32
34
  end
33
35
 
34
36
  def image_filename; "tele.gif"; end
@@ -36,6 +38,7 @@ class Teleporter < Entity
36
38
  def draw_zorder; ZOrder::Teleporter end
37
39
 
38
40
  def to_s
41
+ return "#{super} [not in a space]" unless space
39
42
  destinations = space.possessions(self).collect do |d|
40
43
  "#{d.x}x#{d.y}"
41
44
  end.join(', ')
@@ -9,6 +9,6 @@ module EntityConstants
9
9
  # The dimensions of a cell, equals the dimensions of an entity
10
10
  WIDTH = HEIGHT = CELL_WIDTH_IN_PIXELS * PIXEL_WIDTH
11
11
 
12
- # Maximum velocity is a full cell per tick, which is a lot
13
- MAX_VELOCITY = WIDTH
12
+ # Maximum velocity is just shy of a full cell per tick, which is a lot
13
+ MAX_VELOCITY = WIDTH - 1
14
14
  end
@@ -9,7 +9,8 @@ require 'game_2d/server_port'
9
9
  require 'game_2d/game_space'
10
10
  require 'game_2d/serializable'
11
11
  require 'game_2d/entity'
12
- require 'game_2d/player'
12
+ require 'game_2d/entity/gecko'
13
+ require 'game_2d/entity/ghost'
13
14
 
14
15
  WORLD_WIDTH = 100 # in cells
15
16
  WORLD_HEIGHT = 70 # in cells
@@ -38,6 +39,9 @@ class Game
38
39
  nil, # level ID
39
40
  args[:width] || WORLD_WIDTH,
40
41
  args[:height] || WORLD_HEIGHT)
42
+
43
+ @space << Entity::Base.new(*@space.center)
44
+
41
45
  @space.storage = level_storage
42
46
  else
43
47
  @space = GameSpace.load(self, level_storage)
@@ -95,32 +99,49 @@ class Game
95
99
  end
96
100
 
97
101
  def add_player(player_name)
98
- player = Player.new(player_name)
99
- player.x = (@space.width - Entity::WIDTH) / 2
100
- player.y = (@space.height - Entity::HEIGHT) / 2
101
- other_players = @space.players.dup
102
+ if base = @space.available_base
103
+ player = Entity::Gecko.new(player_name)
104
+ player.x, player.y, player.a = base.x, base.y, base.a
105
+ else
106
+ player = Entity::Ghost.new(player_name)
107
+ player.x, player.y = @space.center
108
+ end
102
109
  @space << player
103
- other_players.each {|p| player_connection(p).add_player(player, @tick) }
110
+
111
+ each_player_conn do |c|
112
+ c.add_player(player, @tick) unless c.player_name == player_name
113
+ end
104
114
  player
105
115
  end
106
116
 
107
- def player_id_connection(player_id)
108
- @port.player_connection(player_id)
117
+ def replace_player_entity(player_name, new_player_id)
118
+ conn = player_name_connection(player_name)
119
+ old = conn.player_id
120
+ conn.player_id = new_player_id
121
+ end
122
+
123
+ def player_name_connection(player_name)
124
+ @port.player_name_connection(player_name)
109
125
  end
110
126
 
111
127
  def player_connection(player)
112
- player_id_connection(player.registry_id)
128
+ player_name_connection(player.player_name)
113
129
  end
114
130
 
115
131
  def each_player_conn
116
- get_all_players.each {|p| yield player_connection(p)}
132
+ get_all_players.each {|p| pc = player_connection(p) and yield pc}
133
+ end
134
+
135
+ def send_player_gone(toast)
136
+ @space.doom toast
137
+ each_player_conn {|pc| pc.delete_entity toast, @tick }
117
138
  end
118
139
 
119
- def delete_entity(entity)
120
- puts "Deleting #{entity}"
121
- @space.doom entity
140
+ def delete_entities(entities)
141
+ entities.each do |registry_id|
142
+ @space.doom(@space[registry_id])
143
+ end
122
144
  @space.purge_doomed_entities
123
- each_player_conn {|pc| pc.delete_entity entity, @tick }
124
145
  end
125
146
 
126
147
  # Answering request from client
@@ -135,7 +156,7 @@ class Game
135
156
  entity.update_from_json json
136
157
  entity.grab!
137
158
  else
138
- $stderr.puts "Can't update #{id}, doesn't exist"
159
+ warn "Can't update #{id}, doesn't exist"
139
160
  end
140
161
  end
141
162
  end
@@ -157,13 +178,13 @@ class Game
157
178
  end
158
179
 
159
180
  def add_player_action(action)
160
- at_tick, player_id = action[:at_tick], action[:player_id]
181
+ at_tick, player_name = action[:at_tick], action[:player_name]
161
182
  unless at_tick
162
- $stderr.puts "Received update from #{player_id} without at_tick!"
183
+ warn "Received update from #{player_name} without at_tick!"
163
184
  at_tick = @tick + 1
164
185
  end
165
186
  if at_tick <= @tick
166
- $stderr.puts "Received update from #{player_id} #{@tick + 1 - at_tick} ticks late"
187
+ warn "Received update from #{player_name} #{@tick + 1 - at_tick} ticks late"
167
188
  at_tick = @tick + 1
168
189
  end
169
190
  @player_actions[at_tick] << action
@@ -172,10 +193,16 @@ class Game
172
193
  def process_player_actions
173
194
  if actions = @player_actions.delete(@tick)
174
195
  actions.each do |action|
175
- player_id = action.delete :player_id
196
+ player_name = action.delete :player_name
197
+ conn = player_name_connection(player_name)
198
+ unless conn
199
+ warn "No connection -- dropping move from #{player_name}"
200
+ next
201
+ end
202
+ player_id = conn.player_id
176
203
  player = @space[player_id]
177
204
  unless player
178
- $stderr.puts "No such player #{player_id} -- dropping move"
205
+ warn "No such player #{player_id} -- dropping move from #{player_name}"
179
206
  next
180
207
  end
181
208
  if (move = action[:move])
@@ -184,10 +211,12 @@ class Game
184
211
  add_npcs npcs
185
212
  elsif (entities = action[:update_entities])
186
213
  update_npcs entities
214
+ elsif (entities = action[:delete_entities])
215
+ delete_entities entities
187
216
  elsif (entity_id = action[:snap_to_grid])
188
217
  @space.snap_to_grid entity_id.to_sym
189
218
  else
190
- $stderr.puts "IGNORING BAD DATA from #{player}: #{action.inspect}"
219
+ warn "IGNORING BAD DATA from #{player_name}: #{action.inspect}"
191
220
  end
192
221
  end
193
222
  end
@@ -227,10 +256,11 @@ class Game
227
256
  # N == @registry_broadcast_every
228
257
  def send_full_updates
229
258
  # Set containing brand-new players' IDs
259
+ # This is cleared after we read it
230
260
  new_players = @port.new_players
231
261
 
232
262
  each_player_conn do |pc|
233
- if new_players.include? pc.player_id
263
+ if new_players.include? pc.player_name
234
264
  response = {
235
265
  :you_are => pc.player_id,
236
266
  :world => {
@@ -264,7 +294,7 @@ class Game
264
294
  # This results in something approaching TICKS_PER_SECOND
265
295
  @port.update_until(run_start + Rational(@tick, TICKS_PER_SECOND))
266
296
 
267
- $stderr.puts "Updates per second: #{@tick / (Time.now.to_r - run_start)}" if @profile
297
+ warn "Updates per second: #{@tick / (Time.now.to_r - run_start)}" if @profile
268
298
  end # times
269
299
  end # infinite loop
270
300
  end # run
@@ -7,14 +7,17 @@ require 'gosu'
7
7
 
8
8
  require 'game_2d/client_connection'
9
9
  require 'game_2d/client_engine'
10
- require 'game_2d/game_space'
11
10
  require 'game_2d/entity'
12
11
  require 'game_2d/entity_constants'
12
+ require 'game_2d/entity/base'
13
13
  require 'game_2d/entity/block'
14
- require 'game_2d/entity/titanium'
15
- require 'game_2d/entity/teleporter'
16
14
  require 'game_2d/entity/destination'
17
- require 'game_2d/player'
15
+ require 'game_2d/entity/gecko'
16
+ require 'game_2d/entity/hole'
17
+ require 'game_2d/entity/slime'
18
+ require 'game_2d/entity/teleporter'
19
+ require 'game_2d/entity/titanium'
20
+ require 'game_2d/game_space'
18
21
  require 'game_2d/menu'
19
22
  require 'game_2d/message'
20
23
  require 'game_2d/password_dialog'
@@ -46,11 +49,10 @@ module GameClient
46
49
  DEFAULT_PORT = 4321
47
50
  DEFAULT_KEY_SIZE = 1024
48
51
 
49
- attr_reader :animation, :font, :top_menu
50
- attr_accessor :player_id
52
+ attr_reader :animation, :font, :top_menu, :player_name, :player_id
51
53
 
52
54
  def initialize_from_hash(opts = {})
53
- player_name = opts[:name]
55
+ @player_name = opts[:name]
54
56
  hostname = opts[:hostname]
55
57
  port = opts[:port] || DEFAULT_PORT
56
58
  key_size = opts[:key_size] || DEFAULT_KEY_SIZE
@@ -60,7 +62,7 @@ module GameClient
60
62
  @conn_update_count = @engine_update_count = 0
61
63
  @profile = profile
62
64
 
63
- self.caption = "Game 2D"
65
+ self.caption = "Game 2D - #{@player_name} on #{hostname}"
64
66
 
65
67
  @pressed_buttons = []
66
68
 
@@ -73,7 +75,7 @@ module GameClient
73
75
  @run_start = Time.now.to_f
74
76
  @update_count = 0
75
77
 
76
- @conn = _make_client_connection(hostname, port, self, player_name, key_size)
78
+ @conn = _make_client_connection(hostname, port, self, @player_name, key_size)
77
79
  @engine = @conn.engine = ClientEngine.new(self)
78
80
  @menu = build_top_menu
79
81
  @dialog = PasswordDialog.new(self, @font)
@@ -83,6 +85,8 @@ module GameClient
83
85
  ClientConnection.new(*args)
84
86
  end
85
87
 
88
+ def player_id=(id); @player_id = id.to_sym; end
89
+
86
90
  def display_message(*lines)
87
91
  if @message
88
92
  @message.lines = lines
@@ -134,13 +138,16 @@ module GameClient
134
138
 
135
139
  def object_type_submenus
136
140
  [
137
- ['Dirt', make_block_npc_proc( 5)],
138
- ['Brick', make_block_npc_proc(10)],
139
- ['Cement', make_block_npc_proc(15)],
140
- ['Steel', make_block_npc_proc(20)],
141
- ['Unlikelium', make_block_npc_proc(25)],
142
- ['Titanium', make_block_npc_proc( 0)],
141
+ ['Dirt', make_block_npc_proc( 5) ],
142
+ ['Brick', make_block_npc_proc(10) ],
143
+ ['Cement', make_block_npc_proc(15) ],
144
+ ['Steel', make_block_npc_proc(20) ],
145
+ ['Unlikelium', make_block_npc_proc(25) ],
146
+ ['Titanium', make_block_npc_proc( 0) ],
143
147
  ['Teleporter', make_teleporter_npc_proc],
148
+ ['Hole', make_hole_npc_proc ],
149
+ ['Base', make_base_npc_proc ],
150
+ ['Slime', make_slime_npc_proc ],
144
151
  ].collect do |type_name, p|
145
152
  MenuItem.new(type_name, self, @font) { @create_npc_proc = p }
146
153
  end
@@ -159,6 +166,8 @@ module GameClient
159
166
  end
160
167
 
161
168
  def player
169
+ return unless space
170
+ warn "GameClient#player(): No such entity #{@player_id}" unless space[@player_id]
162
171
  space[@player_id]
163
172
  end
164
173
 
@@ -173,7 +182,7 @@ module GameClient
173
182
  if @profile
174
183
  @conn_update_total += (Time.now.to_f - before_t)
175
184
  @conn_update_count += 1
176
- $stderr.puts "@conn.update() averages #{@conn_update_total / @conn_update_count} seconds each" if (@conn_update_count % 60) == 0
185
+ warn "@conn.update() averages #{@conn_update_total / @conn_update_count} seconds each" if (@conn_update_count % 60) == 0
177
186
  end
178
187
  return unless @engine.world_established?
179
188
 
@@ -182,7 +191,7 @@ module GameClient
182
191
  if @profile
183
192
  @engine_update_total += (Time.now.to_f - before_t)
184
193
  @engine_update_count += 1
185
- $stderr.puts "@engine.update() averages #{@engine_update_total / @engine_update_count} seconds" if (@engine_update_count % 60) == 0
194
+ warn "@engine.update() averages #{@engine_update_total / @engine_update_count} seconds" if (@engine_update_count % 60) == 0
186
195
  end
187
196
 
188
197
  # Player at the keyboard queues up a command
@@ -191,7 +200,7 @@ module GameClient
191
200
 
192
201
  move_grabbed_entity
193
202
 
194
- $stderr.puts "Updates per second: #{@update_count / (Time.now.to_f - @run_start)}" if @profile
203
+ warn "Updates per second: #{@update_count / (Time.now.to_f - @run_start)}" if @profile
195
204
  end
196
205
 
197
206
  def move_grabbed_entity(divide_by = ClientConnection::ACTION_DELAY)
@@ -222,8 +231,8 @@ module GameClient
222
231
  # If handle_click returned anything, the menu consumed the click
223
232
  # If it returned a menu, that's the new one we display
224
233
  @menu = (new_menu.respond_to?(:handle_click) ? new_menu : @top_menu)
225
- else
226
- send_fire
234
+ elsif @player_id
235
+ generate_move_from_click
227
236
  end
228
237
  when Gosu::MsRight then # right-click
229
238
  if button_down?(Gosu::KbRightShift) || button_down?(Gosu::KbLeftShift)
@@ -239,16 +248,19 @@ module GameClient
239
248
  when Gosu::Kb5 then @create_npc_proc = make_block_npc_proc(25).call
240
249
  when Gosu::Kb6 then @create_npc_proc = make_block_npc_proc( 0).call
241
250
  when Gosu::Kb7 then @create_npc_proc = make_teleporter_npc_proc.call
251
+ when Gosu::Kb8 then @create_npc_proc = make_hole_npc_proc.call
252
+ when Gosu::Kb9 then @create_npc_proc = make_base_npc_proc.call
253
+ when Gosu::Kb0 then @create_npc_proc = make_slime_npc_proc.call
254
+ when Gosu::KbDelete then send_delete_entity
255
+ when Gosu::KbBracketLeft then rotate_left
256
+ when Gosu::KbBracketRight then rotate_right
242
257
  else @pressed_buttons << id unless @dialog
243
258
  end
244
259
  end
245
260
 
246
- def send_fire
247
- return unless @player_id
248
- x, y = mouse_coords
249
- x_vel = (x - (player.x + WIDTH / 2)) / PIXEL_WIDTH
250
- y_vel = (y - (player.y + WIDTH / 2)) / PIXEL_WIDTH
251
- @conn.send_move :fire, :x_vel => x_vel, :y_vel => y_vel
261
+ def generate_move_from_click
262
+ move = player.generate_move_from_click(*mouse_coords)
263
+ @conn.send_move(player_id, *move) if move
252
264
  end
253
265
 
254
266
  # X/Y position of the mouse (center of the crosshairs), adjusted for camera
@@ -297,21 +309,41 @@ module GameClient
297
309
  end
298
310
  end
299
311
 
300
- def send_create_npc(type, args)
301
- args.merge!(
312
+ def make_simple_npc_proc(type); proc { send_create_npc "Entity::#{type}" }; end
313
+
314
+ def make_hole_npc_proc; make_simple_npc_proc 'Hole'; end
315
+ def make_base_npc_proc; make_simple_npc_proc 'Base'; end
316
+ def make_slime_npc_proc
317
+ proc do
318
+ send_create_npc 'Entity::Slime', :angle => 270
319
+ end
320
+ end
321
+
322
+ def send_create_npc(type, args={})
323
+ @conn.send_create_npc({
302
324
  :class => type,
303
325
  :position => mouse_entity_location,
304
326
  :velocity => [0, 0],
305
327
  :angle => 0,
306
328
  :moving => true
307
- )
308
- @conn.send_create_npc(args)
329
+ }.merge(args))
309
330
  end
310
331
 
311
332
  def grab_specific(registry_id)
312
333
  @grabbed_entity_id = registry_id
313
334
  end
314
335
 
336
+ # Actions that modify or delete an existing entity will affect:
337
+ # * The grabbed entity, if there is one, or
338
+ # * The entity under the mouse whose center is closest to the mouse
339
+ def selected_object
340
+ if @grabbed_entity_id
341
+ grabbed = space[@grabbed_entity_id]
342
+ return grabbed if grabbed
343
+ end
344
+ space.near_to(*mouse_coords)
345
+ end
346
+
315
347
  def toggle_grab
316
348
  if @grabbed_entity_id
317
349
  @conn.send_snap_to_grid(space[@grabbed_entity_id]) if @snap_to_grid
@@ -321,6 +353,23 @@ module GameClient
321
353
  @grabbed_entity_id = space.near_to(*mouse_coords).nullsafe_registry_id
322
354
  end
323
355
 
356
+ def adjust_angle(adjustment)
357
+ return unless target = selected_object
358
+ @conn.send_update_entity(
359
+ :registry_id => target.registry_id,
360
+ :angle => target.a + adjustment,
361
+ :moving => true # wake it up
362
+ )
363
+ end
364
+
365
+ def rotate_left; adjust_angle(-90); end
366
+ def rotate_right; adjust_angle(+90); end
367
+
368
+ def send_delete_entity
369
+ return unless target = selected_object
370
+ @conn.send_delete_entity target
371
+ end
372
+
324
373
  def shutdown
325
374
  @conn.disconnect
326
375
  close
@@ -328,9 +377,10 @@ module GameClient
328
377
 
329
378
  # Dequeue an input event
330
379
  def handle_input
331
- return if player.falling? || @dialog
380
+ return unless player # can happen when spawning
381
+ return if player.should_fall? || @dialog
332
382
  move = move_for_keypress
333
- @conn.send_move move # also creates a delta in the engine
383
+ @conn.send_move player_id, move # also creates a delta in the engine
334
384
  end
335
385
 
336
386
  # Check keyboard, mouse, and pressed-button queue
@@ -338,23 +388,15 @@ module GameClient
338
388
  def move_for_keypress
339
389
  # Generated once for each keypress
340
390
  until @pressed_buttons.empty?
341
- button = @pressed_buttons.shift
342
- case button
343
- when Gosu::KbUp, Gosu::KbW
344
- return (player.building?) ? :rise_up : :flip
345
- end
391
+ move = player.move_for_keypress(@pressed_buttons.shift)
392
+ return move if move
346
393
  end
347
394
 
348
395
  # Continuously-generated when key held down
349
- case
350
- when button_down?(Gosu::KbLeft), button_down?(Gosu::KbA)
351
- :slide_left
352
- when button_down?(Gosu::KbRight), button_down?(Gosu::KbD)
353
- :slide_right
354
- when button_down?(Gosu::KbRightControl), button_down?(Gosu::KbLeftControl)
355
- :brake
356
- when button_down?(Gosu::KbDown), button_down?(Gosu::KbS)
357
- :build
396
+ player.moves_for_key_held.each do |key, move|
397
+ return move if button_down?(key)
358
398
  end
399
+
400
+ nil
359
401
  end
360
402
  end