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,38 @@
1
+ require 'game_2d/entity'
2
+ require 'game_2d/player'
3
+ require 'game_2d/entity/ghost'
4
+
5
+ class Entity
6
+
7
+ class Base < Entity
8
+ def should_fall?; underfoot.empty?; end
9
+
10
+ def update
11
+ if should_fall?
12
+ self.a = (direction || 180) + 180
13
+ else
14
+ slow_by 1
15
+ end
16
+ super
17
+ end
18
+
19
+ def available?
20
+ return false unless space
21
+
22
+ # Can't use entity.entities_obstructing() here, as that only
23
+ # returns objects opaque to the receiver (the base). Players
24
+ # aren't opaque to bases. We need to ensure there are no
25
+ # solid (non-ghost) players occupying the space.
26
+ #
27
+ # This logic depends on the fact that anything transparent
28
+ # to a base is also transparent to a player. If we ever allow
29
+ # a base to go somewhere a player can't be, that's a problem.
30
+ space.entities_overlapping(x, y).find_all do |e|
31
+ e.is_a?(Player) && !e.is_a?(Entity::Ghost)
32
+ end.empty?
33
+ end
34
+
35
+ def image_filename; "base.png"; end
36
+ end
37
+
38
+ end
@@ -26,9 +26,9 @@ class Block < OwnedEntity
26
26
  when 0
27
27
  true
28
28
  when 1
29
- empty_on_left? || empty_on_right?
29
+ !(supported_on_left && supported_on_right)
30
30
  when 2
31
- empty_on_left? && empty_on_right?
31
+ !(supported_on_left || supported_on_right)
32
32
  when 3
33
33
  empty_on_left? && empty_on_right? && empty_above?
34
34
  when 4
@@ -36,18 +36,87 @@ class Block < OwnedEntity
36
36
  end
37
37
  end
38
38
 
39
+ def supported_on_left
40
+ opaque(space.entities_exactly_at_point(x - WIDTH, y)).any?
41
+ end
42
+
43
+ def supported_on_right
44
+ right_support = opaque(space.entities_exactly_at_point(x + WIDTH, y)).any?
45
+ end
46
+
39
47
  def update
40
48
  if should_fall?
41
- accelerate(0, 1)
49
+ # applies acceleration, but that's all
50
+ space.fall(self)
42
51
  else
43
52
  self.x_vel = self.y_vel = 0
44
53
  end
54
+ # Reduce velocity if necessary, to exactly line up with
55
+ # an upcoming source of support (so we don't move past it)
56
+ if x_vel == 0 && y_vel != 0
57
+ case level
58
+ when 1
59
+ new_y = look_ahead_for_support_both_sides
60
+ self.y_vel = new_y - y if new_y
61
+ when 2
62
+ new_y = look_ahead_for_support_either_side
63
+ self.y_vel = new_y - y if new_y
64
+ end
65
+ end
66
+
45
67
  move
46
68
  end
47
69
 
48
- def harmed_by(other)
49
- puts "#{self}: Ouch!"
50
- self.hp -= 1
70
+ # Need a source of support at the exact same
71
+ # height on both sides
72
+ def look_ahead_for_support_both_sides
73
+ look_ahead_for_support do |left, right|
74
+ left & right
75
+ end
76
+ end
77
+
78
+ # Any source of support will do, either side
79
+ # or both
80
+ def look_ahead_for_support_either_side
81
+ look_ahead_for_support do |left, right|
82
+ left + right
83
+ end
84
+ end
85
+
86
+ # Find the highest (if dropping) or lowest (if rising) height that meets
87
+ # the requirements for support
88
+ def look_ahead_for_support
89
+ support_heights = yield *possible_sources_of_support
90
+
91
+ (y_vel > 0) ? support_heights.min : support_heights.max
92
+ end
93
+
94
+ # Sources of support must intersect with the points next
95
+ # to us, and be:
96
+ # - Level with our lower edge, if dropping
97
+ # - A point above our upper edge, if rising
98
+ # - Close enough that our current velocity will
99
+ # take us past that point during this tick
100
+ #
101
+ # This just returns the heights at which we might find support
102
+ def possible_sources_of_support
103
+ target_height = if y_vel > 0 # dropping
104
+ y + HEIGHT - 1
105
+ else # rising
106
+ y - 1
107
+ end
108
+
109
+ left_support = opaque(space.entities_at_point(x - 1, target_height)).collect(&:y)
110
+ right_support = opaque(space.entities_at_point(x + WIDTH, target_height)).collect(&:y)
111
+
112
+ # Filter out heights we aren't going to reach this tick with
113
+ # our current velocity
114
+ not_too_far = lambda {|its_y| (its_y - y).abs <= y_vel.abs }
115
+ [left_support.find_all(&not_too_far), right_support.find_all(&not_too_far)]
116
+ end
117
+
118
+ def harmed_by(other, damage=1)
119
+ self.hp -= damage
51
120
  @space.doom(self) if hp <= 0
52
121
  end
53
122
 
@@ -6,12 +6,18 @@ class Destination < OwnedEntity
6
6
 
7
7
  def should_fall?; false; end
8
8
 
9
+ def teleportable?; false; end
10
+
9
11
  def update; end
10
12
 
11
13
  def image_filename; "destination.png"; end
12
14
 
13
15
  def draw_zorder; ZOrder::Destination; end
14
16
  def draw_angle; space.game.tick % 360; end
17
+
18
+ def destroy!
19
+ space.doom owner
20
+ end
15
21
  end
16
22
 
17
23
  end
@@ -0,0 +1,237 @@
1
+ require 'facets/kernel/try'
2
+ require 'game_2d/entity'
3
+ require 'game_2d/entity/pellet'
4
+ require 'game_2d/entity/block'
5
+ require 'game_2d/move/rise_up'
6
+ require 'game_2d/player'
7
+
8
+ # A player object that can stick to walls and slide around corners
9
+ # Calling update() causes a move to be dequeued and executed, applying forces
10
+ # to the game object
11
+ class Entity
12
+
13
+ class Gecko < Entity
14
+ include Player
15
+ include Comparable
16
+
17
+ MAX_HP = 1
18
+
19
+ # Game ticks it takes before a block's HP is raised by 1
20
+ BUILD_TIME = 7
21
+
22
+ # Amount to decelerate each tick when braking
23
+ BRAKE_SPEED = 4
24
+
25
+ MOVES_FOR_KEY_HELD = {
26
+ Gosu::KbLeft => :slide_left,
27
+ Gosu::KbA => :slide_left,
28
+ Gosu::KbRight => :slide_right,
29
+ Gosu::KbD => :slide_right,
30
+ Gosu::KbRightControl => :brake,
31
+ Gosu::KbLeftControl => :brake,
32
+ Gosu::KbDown => :build,
33
+ Gosu::KbS => :build,
34
+ }
35
+
36
+ attr_reader :hp, :build_block_id
37
+
38
+ def initialize(player_name = "<unknown>")
39
+ super
40
+ initialize_player
41
+ @player_name = player_name
42
+ @score = 0
43
+ @hp = MAX_HP
44
+ @build_block_id = nil
45
+ @build_level = 0
46
+ end
47
+
48
+ def hp=(p); @hp = [[p, MAX_HP].min, 0].max; end
49
+
50
+ def sleep_now?; false; end
51
+
52
+ def should_fall?; underfoot.empty?; end
53
+
54
+ def build_block_id=(new_id)
55
+ @build_block_id = new_id.try(:to_sym)
56
+ end
57
+
58
+ def building?; @build_block_id; end
59
+
60
+ def build_block
61
+ return nil unless building?
62
+ fail "Can't look up build_block when not in a space" unless @space
63
+ @space[@build_block_id] or fail "Don't have build_block #{@build_block_id}"
64
+ end
65
+
66
+ def harmed_by(other, damage=1)
67
+ self.hp -= damage
68
+ die if hp <= 0
69
+ end
70
+
71
+ def destroy!
72
+ build_block.owner_id = nil if building?
73
+ end
74
+
75
+ def update
76
+ fail "No space set for #{self}" unless @space
77
+ check_for_disown_block
78
+
79
+ return if perform_complex_move
80
+
81
+ if falling = should_fall?
82
+ self.a = 0
83
+ space.fall(self)
84
+ end
85
+
86
+ args = next_move
87
+ case (current_move = args.delete(:move).to_sym)
88
+ when :slide_left, :slide_right, :brake, :flip, :build, :rise_up
89
+ send current_move unless falling
90
+ when :fire
91
+ fire args[:x_vel], args[:y_vel]
92
+ else
93
+ puts "Invalid move for #{self}: #{current_move}, #{args.inspect}"
94
+ end if args
95
+
96
+ # Only go around corner if sitting on exactly one object
97
+ blocks_underfoot = underfoot
98
+ if blocks_underfoot.size == 1
99
+ # Slide around if we're at the corner; otherwise, move normally
100
+ slide_around(blocks_underfoot.first) or move
101
+ else
102
+ # Straddling two objects, or falling
103
+ move
104
+ end
105
+
106
+ # Check again whether we've moved off of a block
107
+ # we were building
108
+ check_for_disown_block
109
+ end
110
+
111
+ def slide_left; slide(self.a - 90); end
112
+ def slide_right; slide(self.a + 90); end
113
+
114
+ def slide(dir)
115
+ if opaque(next_to(dir)).empty?
116
+ accelerate(*angle_to_vector(dir))
117
+ else
118
+ self.a = dir + 180
119
+ end
120
+ end
121
+
122
+ def brake; slow_by BRAKE_SPEED; end
123
+
124
+ def flip; self.a += 180; end
125
+
126
+ # Create the actual pellet
127
+ def fire(x_vel, y_vel)
128
+ pellet = Entity::Pellet.new(@x, @y, 0, x_vel, y_vel)
129
+ pellet.owner = self
130
+ @space << pellet
131
+ end
132
+
133
+ # Create the actual block
134
+ def build
135
+ if building?
136
+ @build_level += 1
137
+ if @build_level >= BUILD_TIME
138
+ @build_level = 0
139
+ build_block.hp += 1
140
+ end
141
+ else
142
+ bb = Entity::Block.new(@x, @y)
143
+ bb.owner_id = registry_id
144
+ bb.hp = 1
145
+ if @space << bb # generates an ID
146
+ @build_block_id = bb.registry_id
147
+ @build_level = 0
148
+ end
149
+ end
150
+ end
151
+
152
+ def disown_block; @build_block_id, @build_level = nil, 0; end
153
+
154
+ def check_for_disown_block
155
+ return unless building?
156
+ return if @space.entities_overlapping(@x, @y).include?(build_block)
157
+ build_block.owner_id = nil
158
+ build_block.wake!
159
+ disown_block
160
+ end
161
+
162
+ def rise_up
163
+ self.complex_move = Move::RiseUp.new(self)
164
+ end
165
+
166
+ # Called by GameWindow
167
+ # Should return the move to be sent via ClientConnection
168
+ # (or nil)
169
+ def generate_move_from_click(x, y)
170
+ if y < cy # Firing up
171
+ y_vel = -Math.sqrt(2 * (cy - y)).round
172
+ x_vel = (cx - x) / y_vel
173
+ else
174
+ y_vel = 0
175
+ if y == cy
176
+ return if x == cx
177
+ x_vel = (x <=> cx) * MAX_VELOCITY
178
+ else
179
+ range = x - cx
180
+ x_vel = (Math.sqrt(1.0 / (2.0 * (y - cy))) * range).round
181
+ end
182
+ end
183
+ [:fire, {:x_vel => x_vel, :y_vel => y_vel}]
184
+ end
185
+
186
+ # Called by GameWindow
187
+ # Should return the move to be sent via ClientConnection
188
+ # (or nil)
189
+ # This is for queued keypresses, i.e. those that happen
190
+ # on key-down only (just once for a press), not continuously
191
+ # for as long as held down
192
+ def move_for_keypress(keypress)
193
+ case keypress
194
+ when Gosu::KbUp, Gosu::KbW
195
+ return building? ? :rise_up : :flip
196
+ end
197
+ end
198
+
199
+ # Called by GameWindow
200
+ # Should return a map where the keys are... keys, and the
201
+ # values are the corresponding moves to be sent via
202
+ # ClientConnection
203
+ # This is for non-queued keypresses, i.e. those that happen
204
+ # continuously for as long as held down
205
+ def moves_for_key_held
206
+ MOVES_FOR_KEY_HELD
207
+ end
208
+
209
+ def all_state
210
+ # Player name goes first, so we can sort on that
211
+ super.unshift(player_name).push(
212
+ score, @hp, build_block_id, @complex_move)
213
+ end
214
+
215
+ def as_json
216
+ super.merge!(
217
+ :player_name => player_name,
218
+ :score => score,
219
+ :hp => @hp,
220
+ :build_block => @build_block_id,
221
+ :complex_move => @complex_move.as_json
222
+ )
223
+ end
224
+
225
+ def update_from_json(json)
226
+ @player_name = json[:player_name] if json[:player_name]
227
+ @score = json[:score] if json[:score]
228
+ @hp = json[:hp] if json[:hp]
229
+ @build_block_id = json[:build_block].try(:to_sym) if json[:build_block]
230
+ @complex_move = Serializable.from_json(json[:complex_move]) if json[:complex_move]
231
+ super
232
+ end
233
+
234
+ def image_filename; "gecko.png"; end
235
+ end
236
+
237
+ end
@@ -0,0 +1,126 @@
1
+ require 'game_2d/entity'
2
+ require 'game_2d/move/spawn'
3
+ require 'game_2d/player'
4
+
5
+ # A player object that represents the player when between corporeal
6
+ # incarnations
7
+ #
8
+ # Ghost can fly around and look at things, but can't touch or affect
9
+ # anything
10
+ class Entity
11
+
12
+ class Ghost < Entity
13
+ include Player
14
+ include Comparable
15
+
16
+ MOVES_FOR_KEY_HELD = {
17
+ Gosu::KbLeft => :left,
18
+ Gosu::KbA => :left,
19
+ Gosu::KbRight => :right,
20
+ Gosu::KbD => :right,
21
+ Gosu::KbUp => :up,
22
+ Gosu::KbW => :up,
23
+ Gosu::KbDown => :down,
24
+ Gosu::KbS => :down,
25
+ }
26
+
27
+ def initialize(player_name = "<unknown>")
28
+ super
29
+ initialize_player
30
+ @player_name = player_name
31
+ @score = 0
32
+ end
33
+
34
+ def sleep_now?; false; end
35
+
36
+ def should_fall?; false; end
37
+
38
+ def teleportable?; false; end
39
+
40
+ def update
41
+ fail "No space set for #{self}" unless @space
42
+
43
+ return if perform_complex_move
44
+
45
+ if args = next_move
46
+ case (current_move = args.delete(:move).to_sym)
47
+ when :left, :right, :up, :down
48
+ send current_move
49
+ when :spawn
50
+ spawn args[:x], args[:y]
51
+ else
52
+ puts "Invalid move for #{self}: #{current_move}, #{args.inspect}"
53
+ end
54
+ else
55
+ slow_by 1
56
+ end
57
+ super
58
+ end
59
+
60
+ def left; accelerate(-1, 0); end
61
+ def right; accelerate(1, 0); end
62
+ def up; accelerate(0, -1); end
63
+ def down; accelerate(0, 1); end
64
+
65
+ def spawn(x, y)
66
+ if base = @space.available_base_near(x, y)
67
+ warn "#{self} spawning at #{base.x}, #{base.y}"
68
+ self.complex_move = Move::Spawn.new
69
+ self.complex_move.target_id = base.registry_id
70
+ end
71
+ end
72
+
73
+ # Called by GameWindow
74
+ # Should return a map where the keys are... keys, and the
75
+ # values are the corresponding moves to be sent via
76
+ # ClientConnection
77
+ # This is for non-queued keypresses, i.e. those that happen
78
+ # continuously for as long as held down
79
+ def moves_for_key_held
80
+ MOVES_FOR_KEY_HELD
81
+ end
82
+
83
+ # Called by GameWindow
84
+ # Should return the move to be sent via ClientConnection
85
+ # (or nil)
86
+ # This is for queued keypresses, i.e. those that happen
87
+ # on key-down only (just once for a press), not continuously
88
+ # for as long as held down
89
+ def move_for_keypress(keypress); nil; end
90
+
91
+ # Called by GameWindow
92
+ # Should return the move to be sent via ClientConnection
93
+ # (or nil)
94
+ def generate_move_from_click(x, y)
95
+ [:spawn, {:x => x, :y => y}]
96
+ end
97
+
98
+ def all_state
99
+ # Player name goes first, so we can sort on that
100
+ super.unshift(player_name).push(score, @complex_move)
101
+ end
102
+
103
+ def as_json
104
+ super.merge!(
105
+ :player_name => player_name,
106
+ :score => score,
107
+ :complex_move => @complex_move.as_json
108
+ )
109
+ end
110
+
111
+ def update_from_json(json)
112
+ @player_name = json[:player_name] if json[:player_name]
113
+ @score = json[:score] if json[:score]
114
+ @complex_move = Serializable.from_json(json[:complex_move]) if json[:complex_move]
115
+ super
116
+ end
117
+
118
+ def image_filename; "ghost.png"; end
119
+
120
+ def draw_image(anim)
121
+ # Usually frame 0, occasionally frame 1
122
+ anim[((Gosu::milliseconds / 100) % 63) / 62]
123
+ end
124
+ end
125
+
126
+ end