game_2d 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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