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.
@@ -3,9 +3,10 @@ require 'delegate'
3
3
  require 'set'
4
4
  require 'facets/kernel/try'
5
5
  require 'game_2d/wall'
6
- require 'game_2d/player'
6
+ require 'game_2d/entity/gecko'
7
7
  require 'game_2d/serializable'
8
8
  require 'game_2d/entity_constants'
9
+ require 'game_2d/entity/base'
9
10
  require 'game_2d/entity/owned_entity'
10
11
 
11
12
  # Common code between the server and client for maintaining the world.
@@ -34,11 +35,12 @@ require 'game_2d/entity/owned_entity'
34
35
  class Cell < DelegateClass(Array)
35
36
  attr_reader :x, :y
36
37
 
38
+ SORT_BY_REGISTRY = ->(a,b) { a.registry_id <=> b.registry_id }
37
39
  def ==(other)
38
40
  other.class.equal?(self.class) &&
39
41
  other.x == self.x &&
40
42
  other.y == self.y &&
41
- other.instance_variable_get(:@a) == @a
43
+ other.instance_variable_get(:@a).sort(&SORT_BY_REGISTRY) == @a.sort(&SORT_BY_REGISTRY)
42
44
  end
43
45
 
44
46
  def initialize(cell_x, cell_y)
@@ -78,6 +80,8 @@ class GameSpace
78
80
 
79
81
  @players = []
80
82
  @npcs = []
83
+ @bases = []
84
+ @gravities = []
81
85
  end
82
86
 
83
87
  # Width and height, measured in cells
@@ -122,7 +126,7 @@ class GameSpace
122
126
  @game, @storage = original.game, original.storage
123
127
 
124
128
  # Registry should contain all objects - clone those
125
- original.all_registered.each {|ent| self << ent.clone }
129
+ original.all_registered.each {|ent| add_entity ent.clone }
126
130
 
127
131
  self
128
132
  end
@@ -161,6 +165,9 @@ class GameSpace
161
165
  def width; @cell_width * WIDTH; end
162
166
  def height; @cell_height * HEIGHT; end
163
167
 
168
+ # Where to place an entity if you want it dead-center
169
+ def center; [(width - Entity::WIDTH) / 2, (height - Entity::HEIGHT) / 2]; end
170
+
164
171
  def next_id
165
172
  "R#{@highest_id += 1}".to_sym
166
173
  end
@@ -202,6 +209,8 @@ class GameSpace
202
209
  @registry[reg_id] = entity
203
210
  entity_list(entity) << entity
204
211
  register_with_owner(entity)
212
+ register_gravity(entity)
213
+ register_base(entity)
205
214
  nil
206
215
  end
207
216
 
@@ -214,6 +223,8 @@ class GameSpace
214
223
 
215
224
  def deregister(entity)
216
225
  fail "#{entity} not registered" unless registered?(entity)
226
+ deregister_base(entity)
227
+ deregister_gravity(entity)
217
228
  deregister_ownership(entity)
218
229
  entity_list(entity).delete entity
219
230
  @registry.delete entity.registry_id
@@ -231,6 +242,40 @@ class GameSpace
231
242
  @ownership.delete entity.registry_id
232
243
  end
233
244
 
245
+ def register_gravity(entity)
246
+ return unless entity.respond_to? :apply_gravity_to?
247
+ @gravities.unshift entity.registry_id
248
+ end
249
+
250
+ def register_base(entity)
251
+ return unless entity.is_a? Entity::Base
252
+ @bases.unshift entity.registry_id
253
+ end
254
+
255
+ def deregister_gravity(entity)
256
+ @gravities.delete entity.registry_id
257
+ end
258
+
259
+ def deregister_base(entity)
260
+ @bases.delete entity.registry_id
261
+ end
262
+
263
+ # Return a list of available bases, for a player to (re)spawn
264
+ def available_bases
265
+ @bases.collect {|id| self[id] }.find_all(&:available?)
266
+ end
267
+
268
+ # Return a "randomly" chosen available base, or nil
269
+ def available_base
270
+ choices = available_bases
271
+ choices.empty? ? nil : choices[game.tick % choices.size]
272
+ end
273
+
274
+ # Return the available base closest to the coordinates, or nil
275
+ def available_base_near(x, y)
276
+ nearest_to(available_bases, x, y)
277
+ end
278
+
234
279
  def owner_change(owned_id, old_owner_id, new_owner_id)
235
280
  return unless owned_id
236
281
  return if old_owner_id == new_owner_id
@@ -292,7 +337,18 @@ class GameSpace
292
337
  ]
293
338
  end
294
339
 
295
- # Return a list of the entities (if any) at a subpixel point (X, Y)
340
+ # Return a list of the entities (if any) exactly at
341
+ # the subpixel point (X, Y). That is, the point is
342
+ # the entity's upper-left corner
343
+ def entities_exactly_at_point(x, y)
344
+ at(*cell_location_at_point(x, y)).find_all do |e|
345
+ e.x == x && e.y == y
346
+ end
347
+ end
348
+
349
+ # Return a list of the entities (if any) intersecting with
350
+ # the subpixel point (X, Y). That is, the point falls
351
+ # somewhere within the entity
296
352
  def entities_at_point(x, y)
297
353
  at(*cell_location_at_point(x, y)).find_all do |e|
298
354
  e.x <= x && e.x > (x - WIDTH) &&
@@ -300,15 +356,26 @@ class GameSpace
300
356
  end
301
357
  end
302
358
 
359
+ def distance_between(x1, y1, x2, y2)
360
+ delta_x = (x1 - x2).abs
361
+ delta_y = (y1 - y2).abs
362
+ Math.sqrt(delta_x**2 + delta_y**2)
363
+ end
364
+
365
+ # Consider a given list of entities
366
+ # Return whichever entity's center is closest (or ties for closest)
367
+ # to the given coordinates
368
+ def nearest_to(entities, x, y)
369
+ entities.collect do |entity|
370
+ [distance_between(entity.cx, entity.cy, x, y), entity]
371
+ end.sort {|(d1, e1), (d2, e2)| d1 <=> d2}.first.try(:last)
372
+ end
373
+
374
+ # Consider all entities intersecting with (x, y)
303
375
  # Return whichever entity's center is closest (or ties for closest)
304
376
  def near_to(x, y)
305
377
  entities_at_point(x, y).collect do |entity|
306
- center_x = entity.x + WIDTH/2
307
- center_y = entity.y + HEIGHT/2
308
- delta_x = (center_x - x).abs
309
- delta_y = (center_y - y).abs
310
- distance = Math.sqrt(delta_x**2 + delta_y**2)
311
- [distance, entity]
378
+ [distance_between(entity.cx, entity.cy, x, y), entity]
312
379
  end.sort {|(d1, e1), (d2, e2)| d1 <=> d2}.first.try(:last)
313
380
  end
314
381
 
@@ -402,6 +469,16 @@ class GameSpace
402
469
  moved
403
470
  end
404
471
 
472
+ # Add the entity to the grid, and register it
473
+ # For use only during copies and registry syncs -- some
474
+ # checks are skipped, and neighbors aren't woken up
475
+ def add_entity(entity)
476
+ entity.space = self
477
+ register(entity)
478
+ add_entity_to_grid(entity)
479
+ entity
480
+ end
481
+
405
482
  # Add an entity. Will wake neighboring entities
406
483
  def <<(entity)
407
484
  entity.registry_id = next_id unless entity.registry_id?
@@ -412,20 +489,18 @@ class GameSpace
412
489
  entity.space = self
413
490
  conflicts = entity.entities_obstructing(entity.x, entity.y)
414
491
  if conflicts.empty?
415
- register(entity)
416
- add_entity_to_grid(entity)
417
492
  entities_bordering_entity_at(entity.x, entity.y).each(&:wake!)
418
- entity
493
+ add_entity(entity)
419
494
  else
420
495
  entity.space = nil
421
496
  # TODO: Convey error to user somehow
422
- $stderr.puts "Can't create #{entity}, occupied by #{conflicts.inspect}"
497
+ warn "Can't create #{entity}, occupied by #{conflicts.inspect}"
423
498
  end
424
499
  end
425
500
 
426
501
  def snap_to_grid(entity_id)
427
502
  unless entity = self[entity_id]
428
- $stderr.puts "Can't snap #{entity_id}, doesn't exist"
503
+ warn "Can't snap #{entity_id}, doesn't exist"
429
504
  return
430
505
  end
431
506
 
@@ -443,11 +518,21 @@ class GameSpace
443
518
  return
444
519
  end
445
520
  end
446
- $stderr.puts "Couldn't snap #{entity} to grid"
521
+ warn "Couldn't snap #{entity} to grid"
522
+ end
523
+
524
+ def fall(entity)
525
+ return if @gravities.find {|g| self[g].apply_gravity_to?(entity)}
526
+ entity.accelerate(0, 1)
447
527
  end
448
528
 
449
529
  # Doom an entity (mark it to be deleted but don't remove it yet)
450
- def doom(entity); @doomed << entity; end
530
+ def doom(entity)
531
+ return unless entity && registered?(entity)
532
+ return if doomed?(entity)
533
+ @doomed << entity
534
+ entity.destroy!
535
+ end
451
536
 
452
537
  def doomed?(entity); @doomed.include?(entity); end
453
538
 
@@ -459,10 +544,9 @@ class GameSpace
459
544
  def purge_doomed_entities
460
545
  @doomed.each do |entity|
461
546
  if registered?(entity)
462
- entity.destroy!
463
- deregister(entity)
464
- entities_bordering_entity_at(entity.x, entity.y).each(&:wake!)
465
547
  remove_entity_from_grid(entity)
548
+ entities_bordering_entity_at(entity.x, entity.y).each(&:wake!)
549
+ deregister(entity)
466
550
  else
467
551
  fire_entity_not_found(entity)
468
552
  end
@@ -471,15 +555,22 @@ class GameSpace
471
555
  end
472
556
 
473
557
  def update
558
+ grabbed_entities = []
474
559
  @registry.values.each do |ent|
475
560
  if ent.grabbed?
476
- ent.move
477
- ent.release!
478
- ent.x_vel = ent.y_vel = 0
561
+ grabbed_entities << ent
479
562
  elsif ent.moving?
480
563
  ent.update
481
564
  end
482
565
  end
566
+ # Update these, and clear their flag, last
567
+ # Gives other entities (e.g. teleporters) a chance to
568
+ # consider their grabbed-state
569
+ grabbed_entities.each do |ent|
570
+ ent.move
571
+ ent.release!
572
+ ent.x_vel = ent.y_vel = 0
573
+ end
483
574
  purge_doomed_entities
484
575
  end
485
576
 
@@ -43,9 +43,9 @@ class GameWindow < Gosu::Window
43
43
  ZOrder::Cursor,
44
44
  1, 1, Gosu::Color::WHITE, :add)
45
45
 
46
- return unless @player_id
46
+ return unless p = player
47
47
 
48
- @camera_x, @camera_y = space.good_camera_position_for(player, SCREEN_WIDTH, SCREEN_HEIGHT)
48
+ @camera_x, @camera_y = space.good_camera_position_for(p, SCREEN_WIDTH, SCREEN_HEIGHT)
49
49
  translate(-@camera_x, -@camera_y) do
50
50
  (space.players + space.npcs).each {|entity| entity.draw(self) }
51
51
  end
@@ -0,0 +1,67 @@
1
+ require 'game_2d/complex_move'
2
+ require 'game_2d/game_client'
3
+ require 'game_2d/entity_constants'
4
+ require 'game_2d/entity/gecko'
5
+
6
+ module Move
7
+
8
+ # A move for ghosts.
9
+ class Spawn < ComplexMove
10
+ include EntityConstants
11
+
12
+ SPAWN_TRAVEL_SPEED = 80
13
+
14
+ # target_id is registry_id of selected base
15
+ attr_accessor :target_id
16
+
17
+ def on_completion(actor)
18
+ space = actor.space
19
+ target = space[target_id]
20
+ return unless target && target.available?
21
+
22
+ gecko = Entity::Gecko.new(actor.player_name)
23
+ gecko.score = actor.score
24
+ gecko.x, gecko.y, gecko.a = target.x, target.y, target.a
25
+ return unless space << gecko
26
+
27
+ actor.replace_player_entity(gecko)
28
+ end
29
+
30
+ def update(actor)
31
+ # It's convenient to set 'self' to the Player
32
+ # object, here
33
+ actor.instance_exec(self) do |cm|
34
+ # Abort if the target gets destroyed, or becomes
35
+ # occupied
36
+ target = space[cm.target_id]
37
+ return false unless target && target.available?
38
+
39
+ # We're done
40
+ if x == target.x && y == target.y
41
+ @x_vel = @y_vel = 0
42
+ return false
43
+ end
44
+
45
+ @x_vel = [[target.x - x, -SPAWN_TRAVEL_SPEED].max, SPAWN_TRAVEL_SPEED].min
46
+ @y_vel = [[target.y - y, -SPAWN_TRAVEL_SPEED].max, SPAWN_TRAVEL_SPEED].min
47
+ # move returns false: it failed somehow
48
+ return move
49
+ end
50
+ end
51
+
52
+ def all_state
53
+ super.push @target_id
54
+ end
55
+ def as_json
56
+ super.merge! :target => @target_id
57
+ end
58
+ def update_from_json(json)
59
+ self.target_id = json[:target].to_sym if json[:target]
60
+ super
61
+ end
62
+ def to_s
63
+ "Spawn[#{target_id}]"
64
+ end
65
+ end
66
+
67
+ end
@@ -1,205 +1,32 @@
1
- require 'facets/kernel/try'
2
1
  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'
2
+ require 'game_2d/entity_constants'
7
3
  require 'game_2d/zorder'
8
4
 
9
- # The base Player class representing what all Players have in common
5
+ # The base module representing what all Players have in common
10
6
  # 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
7
+ # The server instantiates classes that mix in this module, to
8
+ # represent each connected player
9
+ module Player
10
+ include EntityConstants
17
11
 
18
- # Game ticks it takes before a block's HP is raised by 1
19
- BUILD_TIME = 7
12
+ attr_accessor :player_name, :score, :complex_move
20
13
 
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
14
+ def initialize_player
31
15
  @moves = []
32
- @current_move = nil
33
- @build_block_id = nil
34
- @build_level = 0
35
16
  @complex_move = nil
36
17
  end
37
18
 
38
- def sleep_now?; false; end
39
-
40
- def underfoot
41
- opaque(next_to(self.a + 180))
42
- end
43
- def falling?
44
- underfoot.empty?
45
- end
46
-
47
- def build_block_id=(new_id)
48
- @build_block_id = new_id.try(:to_sym)
49
- end
50
-
51
- def building?; @build_block_id; end
52
-
53
- def build_block
54
- return nil unless building?
55
- fail "Can't look up build_block when not in a space" unless @space
56
- @space[@build_block_id] or fail "Don't have build_block #{@build_block_id}"
57
- end
58
-
59
- def destroy!
60
- build_block.owner_id = nil if building?
61
- end
62
-
63
- def update
64
- fail "No space set for #{self}" unless @space
65
- check_for_disown_block
66
-
67
- if @complex_move
68
- # returns true if more work to do
69
- return if @complex_move.update(self)
70
- @complex_move.on_completion(self)
71
- @complex_move = nil
72
- end
73
-
74
- if falling = falling?
75
- self.a = 0
76
- accelerate(0, 1)
77
- end
78
-
79
- args = @moves.shift
80
- case (current_move = args.delete(:move).to_sym)
81
- when :slide_left, :slide_right, :brake, :flip, :build, :rise_up
82
- send current_move unless falling
83
- when :fire
84
- fire args[:x_vel], args[:y_vel]
85
- else
86
- puts "Invalid move for #{self}: #{current_move}, #{args.inspect}"
87
- end if args
19
+ # Returns true if a complex move is in process, and took
20
+ # some action
21
+ # Returns nil if the complex move completed, or there isn't one
22
+ def perform_complex_move
23
+ return unless @complex_move
88
24
 
89
- # Only go around corner if sitting on exactly one object
90
- blocks_underfoot = underfoot
91
- if blocks_underfoot.size == 1
92
- other = blocks_underfoot.first
93
- # Figure out where corner is and whether we're about to reach or pass it
94
- corner, distance, overshoot, turn = going_past_entity(other.x, other.y)
95
- if corner
96
- original_speed = @x_vel.abs + @y_vel.abs
97
- original_dir = vector_to_angle
98
- new_dir = original_dir + turn
25
+ # returns true if more work to do
26
+ return true if @complex_move.update(self)
99
27
 
100
- # Make sure nothing occupies any space we're about to move through
101
- if opaque(
102
- @space.entities_overlapping(*corner) + next_to(new_dir, *corner)
103
- ).empty?
104
- # Move to the corner
105
- self.x_vel, self.y_vel = angle_to_vector(original_dir, distance)
106
- move
107
-
108
- # Turn and apply remaining velocity
109
- # Make sure we move at least one subpixel so we don't sit exactly at
110
- # the corner, and fall
111
- self.a += turn
112
- overshoot = 1 if overshoot.zero?
113
- self.x_vel, self.y_vel = angle_to_vector(new_dir, overshoot)
114
- move
115
-
116
- self.x_vel, self.y_vel = angle_to_vector(new_dir, original_speed)
117
- else
118
- # Something's in the way -- possibly in front of us, or possibly
119
- # around the corner
120
- move
121
- end
122
- else
123
- # Not yet reaching the corner -- or making a diagonal motion, for which
124
- # we can't support going around the corner
125
- move
126
- end
127
- else
128
- # Straddling two objects, or falling
129
- move
130
- end
131
-
132
- # Check again whether we've moved off of a block
133
- # we were building
134
- check_for_disown_block
135
- end
136
-
137
- def slide_left; slide(self.a - 90); end
138
- def slide_right; slide(self.a + 90); end
139
-
140
- def slide(dir)
141
- if opaque(next_to(dir)).empty?
142
- accelerate(*angle_to_vector(dir))
143
- else
144
- self.a = dir + 180
145
- end
146
- end
147
-
148
- def brake
149
- if @x_vel.zero?
150
- self.y_vel = brake_velocity(@y_vel)
151
- else
152
- self.x_vel = brake_velocity(@x_vel)
153
- end
154
- end
155
-
156
- def brake_velocity(v)
157
- return 0 if v.abs < BRAKE_SPEED
158
- sign = v <=> 0
159
- sign * (v.abs - BRAKE_SPEED)
160
- end
161
-
162
- def flip
163
- self.a += 180
164
- end
165
-
166
- # Create the actual pellet
167
- def fire(x_vel, y_vel)
168
- pellet = Entity::Pellet.new(@x, @y, 0, x_vel, y_vel)
169
- pellet.owner = self
170
- @space << pellet
171
- end
172
-
173
- # Create the actual block
174
- def build
175
- if building?
176
- @build_level += 1
177
- if @build_level >= BUILD_TIME
178
- @build_level = 0
179
- build_block.hp += 1
180
- end
181
- else
182
- bb = Entity::Block.new(@x, @y)
183
- bb.owner_id = registry_id
184
- bb.hp = 1
185
- @space << bb # generates an ID
186
- @build_block_id = bb.registry_id
187
- @build_level = 0
188
- end
189
- end
190
-
191
- def disown_block; $stderr.puts "#{self} disowning #{build_block}"; @build_block_id, @build_level = nil, 0; end
192
-
193
- def check_for_disown_block
194
- return unless building?
195
- return if @space.entities_overlapping(@x, @y).include?(build_block)
196
- build_block.owner_id = nil
197
- build_block.wake!
198
- disown_block
199
- end
200
-
201
- def rise_up
202
- @complex_move = Move::RiseUp.new(self)
28
+ @complex_move.on_completion(self)
29
+ @complex_move = nil
203
30
  end
204
31
 
205
32
  # Accepts a hash, with a key :move => move_type
@@ -208,34 +35,26 @@ class Player < Entity
208
35
  @moves << new_move
209
36
  end
210
37
 
211
- def to_s
212
- "#{player_name} (#{registry_id_safe}) at #{x}x#{y}"
213
- end
38
+ def next_move; @moves.shift; end
214
39
 
215
- def all_state
216
- super.unshift(player_name).push(
217
- score, build_block_id, @complex_move)
40
+ def replace_player_entity(new_entity)
41
+ if (game = space.game).is_a? GameClient
42
+ game.player_id = new_entity.registry_id if game.player_id == registry_id
43
+ else
44
+ game.replace_player_entity(player_name, new_entity.registry_id)
45
+ end
46
+ space.doom(self)
218
47
  end
219
48
 
220
- def as_json
221
- super.merge!(
222
- :player_name => player_name,
223
- :score => score,
224
- :build_block => @build_block_id,
225
- :complex_move => @complex_move.as_json
226
- )
227
- end
49
+ def die
50
+ ghost = Entity::Ghost.new(player_name)
51
+ ghost.x, ghost.y, ghost.a, ghost.x_vel, ghost.y_vel, ghost.score =
52
+ x, y, 0, x_vel, y_vel, score
53
+ return unless space << ghost # coast to coast
228
54
 
229
- def update_from_json(json)
230
- @player_name = json[:player_name] if json[:player_name]
231
- @score = json[:score] if json[:score]
232
- @build_block_id = json[:build_block].try(:to_sym) if json[:build_block]
233
- @complex_move = Serializable.from_json(json[:complex_move]) if json[:complex_move]
234
- super
55
+ replace_player_entity ghost
235
56
  end
236
57
 
237
- def image_filename; "player.png"; end
238
-
239
58
  def draw_zorder; ZOrder::Player end
240
59
 
241
60
  def draw(window)
@@ -245,4 +64,8 @@ class Player < Entity
245
64
  0.5, 1.0, # Centered X; above Y
246
65
  1.0, 1.0, Gosu::Color::YELLOW)
247
66
  end
248
- end
67
+
68
+ def to_s
69
+ "#{player_name} (#{self.class.name} #{registry_id_safe}) at #{x}x#{y}"
70
+ end
71
+ end