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.
- data/.gitignore +15 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +84 -0
- data/Rakefile +1 -0
- data/bin/game_2d_client.rb +17 -0
- data/bin/game_2d_server.rb +21 -0
- data/game_2d.gemspec +32 -0
- data/lib/game_2d/client_connection.rb +127 -0
- data/lib/game_2d/client_engine.rb +227 -0
- data/lib/game_2d/complex_move.rb +45 -0
- data/lib/game_2d/entity.rb +371 -0
- data/lib/game_2d/entity/block.rb +73 -0
- data/lib/game_2d/entity/owned_entity.rb +29 -0
- data/lib/game_2d/entity/pellet.rb +27 -0
- data/lib/game_2d/entity/titanium.rb +11 -0
- data/lib/game_2d/entity_constants.rb +14 -0
- data/lib/game_2d/game.rb +213 -0
- data/lib/game_2d/game_space.rb +462 -0
- data/lib/game_2d/game_window.rb +260 -0
- data/lib/game_2d/hash.rb +11 -0
- data/lib/game_2d/menu.rb +82 -0
- data/lib/game_2d/move/rise_up.rb +77 -0
- data/lib/game_2d/player.rb +251 -0
- data/lib/game_2d/registerable.rb +25 -0
- data/lib/game_2d/serializable.rb +69 -0
- data/lib/game_2d/server_connection.rb +104 -0
- data/lib/game_2d/server_port.rb +74 -0
- data/lib/game_2d/storage.rb +42 -0
- data/lib/game_2d/version.rb +3 -0
- data/lib/game_2d/wall.rb +21 -0
- data/lib/game_2d/zorder.rb +3 -0
- data/media/Beep.wav +0 -0
- data/media/Space.png +0 -0
- data/media/Star.png +0 -0
- data/media/Starfighter.bmp +0 -0
- data/media/brick.gif +0 -0
- data/media/cement.gif +0 -0
- data/media/crosshair.gif +0 -0
- data/media/dirt.gif +0 -0
- data/media/pellet.png +0 -0
- data/media/pellet.xcf +0 -0
- data/media/player.png +0 -0
- data/media/player.xcf +0 -0
- data/media/rock.png +0 -0
- data/media/rock.xcf +0 -0
- data/media/steel.gif +0 -0
- data/media/tele.gif +0 -0
- data/media/titanium.gif +0 -0
- data/media/unlikelium.gif +0 -0
- data/spec/client_engine_spec.rb +235 -0
- data/spec/game_space_spec.rb +347 -0
- metadata +246 -0
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'facets/kernel/try'
|
2
|
+
require 'game_2d/serializable'
|
3
|
+
|
4
|
+
# A complex move is any move that has its own state.
|
5
|
+
# Moves that span multiple ticks are complex, because
|
6
|
+
# the server may have to tell the client how much of
|
7
|
+
# the complex move has been completed by a player.
|
8
|
+
class ComplexMove
|
9
|
+
include Serializable
|
10
|
+
|
11
|
+
attr_reader :actor_id
|
12
|
+
|
13
|
+
def initialize(actor=nil)
|
14
|
+
self.actor_id = actor.nullsafe_registry_id
|
15
|
+
end
|
16
|
+
|
17
|
+
def actor_id=(id)
|
18
|
+
@actor_id = id.try(:to_sym)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Execute one tick of the move.
|
22
|
+
# Return true if there is more work to do,
|
23
|
+
# false if the move has completed.
|
24
|
+
def update(actor)
|
25
|
+
false
|
26
|
+
end
|
27
|
+
|
28
|
+
# Take a final action after the complex move
|
29
|
+
# is done
|
30
|
+
def on_completion(actor); end
|
31
|
+
|
32
|
+
def all_state
|
33
|
+
[actor_id]
|
34
|
+
end
|
35
|
+
def as_json
|
36
|
+
Serializable.as_json(self).merge!(:actor_id => actor_id)
|
37
|
+
end
|
38
|
+
def update_from_json(json)
|
39
|
+
self.actor_id = json[:actor_id]
|
40
|
+
self
|
41
|
+
end
|
42
|
+
def to_s
|
43
|
+
self.class.name
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,371 @@
|
|
1
|
+
require 'facets/string/pathize'
|
2
|
+
require 'facets/kernel/constant'
|
3
|
+
require 'game_2d/registerable'
|
4
|
+
require 'game_2d/serializable'
|
5
|
+
require 'game_2d/entity_constants'
|
6
|
+
|
7
|
+
class NilClass
|
8
|
+
# Ignore this
|
9
|
+
def wake!; end
|
10
|
+
end
|
11
|
+
|
12
|
+
class Entity
|
13
|
+
include Serializable
|
14
|
+
include Registerable
|
15
|
+
include EntityConstants
|
16
|
+
|
17
|
+
# X and Y position of the top-left corner
|
18
|
+
attr_accessor :x, :y, :moving, :space
|
19
|
+
|
20
|
+
attr_reader :a, :x_vel, :y_vel
|
21
|
+
|
22
|
+
# space: the game space
|
23
|
+
# x, y: position in sub-pixels of the upper-left corner
|
24
|
+
# a: angle, with 0 = up, 90 = right
|
25
|
+
# x_vel, y_vel: velocity in sub-pixels
|
26
|
+
def initialize(x = 0, y = 0, a = 0, x_vel = 0, y_vel = 0)
|
27
|
+
@x, @y, self.a = x, y, a
|
28
|
+
self.x_vel, self.y_vel = x_vel, y_vel
|
29
|
+
@moving = true
|
30
|
+
end
|
31
|
+
|
32
|
+
def a=(angle); @a = angle % 360; end
|
33
|
+
|
34
|
+
# Velocity is constrained to the range -MAX_VELOCITY .. MAX_VELOCITY
|
35
|
+
def x_vel=(xv)
|
36
|
+
@x_vel = [[xv, MAX_VELOCITY].min, -MAX_VELOCITY].max
|
37
|
+
end
|
38
|
+
def y_vel=(yv)
|
39
|
+
@y_vel = [[yv, MAX_VELOCITY].min, -MAX_VELOCITY].max
|
40
|
+
end
|
41
|
+
|
42
|
+
# True if we need to update this entity
|
43
|
+
def moving?; @moving; end
|
44
|
+
|
45
|
+
def doomed?; @space.doomed?(self); end
|
46
|
+
|
47
|
+
# True if this entity can go to sleep now
|
48
|
+
# Only called if update() fails to produce any motion
|
49
|
+
# Default: Sleep if we're not moving and not falling
|
50
|
+
def sleep_now?
|
51
|
+
self.x_vel == 0 && self.y_vel == 0 && !should_fall?
|
52
|
+
end
|
53
|
+
|
54
|
+
def should_fall?
|
55
|
+
raise "should_fall? undefined"
|
56
|
+
end
|
57
|
+
|
58
|
+
# Notify this entity that it must take action
|
59
|
+
def wake!
|
60
|
+
@moving = true
|
61
|
+
end
|
62
|
+
|
63
|
+
# Give this entity a chance to perform clean-up upon destruction
|
64
|
+
def destroy!; end
|
65
|
+
|
66
|
+
# X positions near this entity's
|
67
|
+
# Position in pixels of the upper-left corner
|
68
|
+
def pixel_x; @x / PIXEL_WIDTH; end
|
69
|
+
def pixel_y; @y / PIXEL_WIDTH; end
|
70
|
+
|
71
|
+
# Left-most cell X position occupied
|
72
|
+
def self.left_cell_x_at(x); x / WIDTH; end
|
73
|
+
# TODO: Find a more elegant way to call class methods in Ruby...
|
74
|
+
def left_cell_x(x = @x); self.class.left_cell_x_at(x); end
|
75
|
+
|
76
|
+
# Right-most cell X position occupied
|
77
|
+
# If we're exactly within a column (@x is an exact multiple of WIDTH),
|
78
|
+
# then this equals left_cell_x. Otherwise, it's one higher
|
79
|
+
def self.right_cell_x_at(x); (x + WIDTH - 1) / WIDTH; end
|
80
|
+
def right_cell_x(x = @x); self.class.right_cell_x_at(x); end
|
81
|
+
|
82
|
+
# Top-most cell Y position occupied
|
83
|
+
def self.top_cell_y_at(y); y / HEIGHT; end
|
84
|
+
def top_cell_y(y = @y); self.class.top_cell_y_at(y); end
|
85
|
+
|
86
|
+
# Bottom-most cell Y position occupied
|
87
|
+
def self.bottom_cell_y_at(y); (y + HEIGHT - 1) / HEIGHT; end
|
88
|
+
def bottom_cell_y(y = @y); self.class.bottom_cell_y_at(y); end
|
89
|
+
|
90
|
+
# Returns an array of one, two, or four cell-coordinate tuples
|
91
|
+
# E.g. [[4, 5], [4, 6], [5, 5], [5, 6]]
|
92
|
+
def occupied_cells(x = @x, y = @y)
|
93
|
+
x_array = (left_cell_x(x) .. right_cell_x(x)).to_a
|
94
|
+
y_array = (top_cell_y(y) .. bottom_cell_y(y)).to_a
|
95
|
+
x_array.product(y_array)
|
96
|
+
end
|
97
|
+
|
98
|
+
# Apply acceleration
|
99
|
+
def accelerate(x_accel, y_accel)
|
100
|
+
self.x_vel = @x_vel + x_accel
|
101
|
+
self.y_vel = @y_vel + y_accel
|
102
|
+
end
|
103
|
+
|
104
|
+
# Override to make particular entities transparent to each other
|
105
|
+
def transparent_to_me?(other)
|
106
|
+
other == self
|
107
|
+
end
|
108
|
+
|
109
|
+
def opaque(others)
|
110
|
+
others.delete_if {|obj| transparent_to_me?(obj)}
|
111
|
+
end
|
112
|
+
|
113
|
+
# Wrapper around @space.entities_overlapping
|
114
|
+
# Allows us to remove any entities that are transparent
|
115
|
+
# to us
|
116
|
+
def entities_obstructing(new_x, new_y)
|
117
|
+
fail "No @space set!" unless @space
|
118
|
+
opaque(@space.entities_overlapping(new_x, new_y))
|
119
|
+
end
|
120
|
+
|
121
|
+
# Process one tick of motion, horizontally only
|
122
|
+
def move_x
|
123
|
+
return if doomed?
|
124
|
+
return if @x_vel.zero?
|
125
|
+
new_x = @x + @x_vel
|
126
|
+
impacts = entities_obstructing(new_x, @y)
|
127
|
+
if impacts.empty?
|
128
|
+
@x = new_x
|
129
|
+
return
|
130
|
+
end
|
131
|
+
@x = if @x_vel > 0 # moving right
|
132
|
+
# X position of leftmost candidate(s)
|
133
|
+
impact_at_x = impacts.collect(&:x).min
|
134
|
+
impacts.delete_if {|e| e.x > impact_at_x }
|
135
|
+
impact_at_x - WIDTH
|
136
|
+
else # moving left
|
137
|
+
# X position of rightmost candidate(s)
|
138
|
+
impact_at_x = impacts.collect(&:x).max
|
139
|
+
impacts.delete_if {|e| e.x < impact_at_x }
|
140
|
+
impact_at_x + WIDTH
|
141
|
+
end
|
142
|
+
self.x_vel = 0
|
143
|
+
i_hit(impacts)
|
144
|
+
end
|
145
|
+
|
146
|
+
# Process one tick of motion, vertically only
|
147
|
+
def move_y
|
148
|
+
return if doomed?
|
149
|
+
return if @y_vel.zero?
|
150
|
+
new_y = @y + @y_vel
|
151
|
+
impacts = entities_obstructing(@x, new_y)
|
152
|
+
if impacts.empty?
|
153
|
+
@y = new_y
|
154
|
+
return
|
155
|
+
end
|
156
|
+
@y = if @y_vel > 0 # moving down
|
157
|
+
# Y position of highest candidate(s)
|
158
|
+
impact_at_y = impacts.collect(&:y).min
|
159
|
+
impacts.delete_if {|e| e.y > impact_at_y }
|
160
|
+
impact_at_y - HEIGHT
|
161
|
+
else # moving up
|
162
|
+
# Y position of lowest candidate(s)
|
163
|
+
impact_at_y = impacts.collect(&:y).max
|
164
|
+
impacts.delete_if {|e| e.y < impact_at_y }
|
165
|
+
impact_at_y + HEIGHT
|
166
|
+
end
|
167
|
+
self.y_vel = 0
|
168
|
+
i_hit(impacts)
|
169
|
+
end
|
170
|
+
|
171
|
+
# Process one tick of motion. Only called when moving? is true
|
172
|
+
def move
|
173
|
+
# Force evaluation of both update_x and update_y (no short-circuit)
|
174
|
+
# If we're moving faster horizontally, do that first
|
175
|
+
# Otherwise do the vertical move first
|
176
|
+
moved = @space.process_moving_entity(self) do
|
177
|
+
if @x_vel.abs > @y_vel.abs then move_x; move_y
|
178
|
+
else move_y; move_x
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
# Didn't move? Might be time to go to sleep
|
183
|
+
@moving = false if !moved && sleep_now?
|
184
|
+
|
185
|
+
moved
|
186
|
+
end
|
187
|
+
|
188
|
+
# Handle any behavior specific to this entity
|
189
|
+
# Default: Accelerate downward if the subclass says we should fall
|
190
|
+
def update
|
191
|
+
accelerate(0, 1) if should_fall?
|
192
|
+
move
|
193
|
+
end
|
194
|
+
|
195
|
+
# Update position/velocity/angle data, and tell the space about it
|
196
|
+
def warp(x, y, x_vel, y_vel, angle=self.a, moving=@moving)
|
197
|
+
blk = proc do
|
198
|
+
@x, @y, self.x_vel, self.y_vel, self.a, @moving =
|
199
|
+
x, y, x_vel, y_vel, angle, moving
|
200
|
+
end
|
201
|
+
if @space
|
202
|
+
@space.process_moving_entity(self, &blk)
|
203
|
+
else
|
204
|
+
blk.call
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
def i_hit(other)
|
209
|
+
# TODO
|
210
|
+
puts "#{self} hit #{other.inspect}"
|
211
|
+
end
|
212
|
+
|
213
|
+
def harmed_by(other); end
|
214
|
+
|
215
|
+
# Return any entities adjacent to this one in the specified direction
|
216
|
+
def next_to(angle, x=@x, y=@y)
|
217
|
+
points = case angle % 360
|
218
|
+
when 0 then
|
219
|
+
[[x, y - 1], [x + WIDTH - 1, y - 1]]
|
220
|
+
when 90 then
|
221
|
+
[[x + WIDTH, y], [x + WIDTH, y + HEIGHT - 1]]
|
222
|
+
when 180 then
|
223
|
+
[[x, y + HEIGHT], [x + WIDTH - 1, y + HEIGHT]]
|
224
|
+
when 270 then
|
225
|
+
[[x - 1, y], [x - 1, y + HEIGHT - 1]]
|
226
|
+
else puts "Trig unimplemented"; []
|
227
|
+
end
|
228
|
+
@space.entities_at_points(points)
|
229
|
+
end
|
230
|
+
|
231
|
+
def empty_underneath?; opaque(next_to(180)).empty?; end
|
232
|
+
def empty_on_left?; opaque(next_to(270)).empty?; end
|
233
|
+
def empty_on_right?; opaque(next_to(90)).empty?; end
|
234
|
+
def empty_above?; opaque(next_to(0)).empty?; end
|
235
|
+
|
236
|
+
def angle_to_vector(angle, amplitude=1)
|
237
|
+
case angle % 360
|
238
|
+
when 0 then [0, -amplitude]
|
239
|
+
when 90 then [amplitude, 0]
|
240
|
+
when 180 then [0, amplitude]
|
241
|
+
when 270 then [-amplitude, 0]
|
242
|
+
else raise "Trig unimplemented"
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
# Convert x/y to an angle
|
247
|
+
def vector_to_angle(x_vel=@x_vel, y_vel=@y_vel)
|
248
|
+
if x_vel == 0 && y_vel == 0
|
249
|
+
return puts "Zero velocity, no angle"
|
250
|
+
end
|
251
|
+
if x_vel != 0 && y_vel != 0
|
252
|
+
return puts "Diagonal velocity (#{x_vel}x#{y_vel}), no angle"
|
253
|
+
end
|
254
|
+
|
255
|
+
if x_vel.zero?
|
256
|
+
(y_vel > 0) ? 180 : 0
|
257
|
+
else
|
258
|
+
(x_vel > 0) ? 90 : 270
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
# Given a vector with a diagonal, drop the smaller component, returning a
|
263
|
+
# vector that is strictly either horizontal or vertical.
|
264
|
+
def drop_diagonal(x_vel, y_vel)
|
265
|
+
(y_vel.abs > x_vel.abs) ? [0, y_vel] : [x_vel, 0]
|
266
|
+
end
|
267
|
+
|
268
|
+
# Is the other entity basically above us, below us, or on the left or the
|
269
|
+
# right? Returns the angle we should face if we want to face that entity.
|
270
|
+
def direction_to(other_x, other_y)
|
271
|
+
vector_to_angle(*drop_diagonal(other_x - @x, other_y - @y))
|
272
|
+
end
|
273
|
+
|
274
|
+
# Given our current position and velocity (and only if our velocity is not
|
275
|
+
# on a diagonal), are we about to move past the entity at the specified
|
276
|
+
# coordinates? If so, returns:
|
277
|
+
#
|
278
|
+
# 1) The X/Y position of the empty space just past the entity. Assuming the
|
279
|
+
# other entity is adjacent to us, this spot touches corners with the other
|
280
|
+
# entity.
|
281
|
+
#
|
282
|
+
# 2) How far we'd go to reach that point.
|
283
|
+
#
|
284
|
+
# 3) How far past that spot we would go.
|
285
|
+
#
|
286
|
+
# 4) Which way we'd have to turn (delta angle) if moving around the other
|
287
|
+
# entity. Either +90 or -90.
|
288
|
+
def going_past_entity(other_x, other_y)
|
289
|
+
return if @x_vel == 0 && @y_vel == 0
|
290
|
+
return if @x_vel != 0 && @y_vel != 0
|
291
|
+
|
292
|
+
if @x_vel.zero?
|
293
|
+
# Moving vertically. Find target height
|
294
|
+
y_pos = (@y_vel > 0) ? other_y + HEIGHT : other_y - HEIGHT
|
295
|
+
distance = (@y - y_pos).abs
|
296
|
+
overshoot = @y_vel.abs - distance
|
297
|
+
turn = if @y_vel > 0
|
298
|
+
# Going down: Turn left if it's on our right
|
299
|
+
direction_to(other_x, other_y) == 90 ? -90 : 90
|
300
|
+
else
|
301
|
+
# Going up: Turn right if it's on our right
|
302
|
+
direction_to(other_x, other_y) == 90 ? 90 : -90
|
303
|
+
end
|
304
|
+
return [[@x, y_pos], distance, overshoot, turn] if overshoot >= 0
|
305
|
+
else
|
306
|
+
# Moving horizontally. Find target column
|
307
|
+
x_pos = (@x_vel > 0) ? other_x + WIDTH : other_x - WIDTH
|
308
|
+
distance = (@x - x_pos).abs
|
309
|
+
overshoot = @x_vel.abs - distance
|
310
|
+
turn = if @x_vel > 0
|
311
|
+
# Going right: Turn right if it's below us
|
312
|
+
direction_to(other_x, other_y) == 180 ? 90 : -90
|
313
|
+
else
|
314
|
+
# Going left: Turn left if it's below us
|
315
|
+
direction_to(other_x, other_y) == 180 ? -90 : 90
|
316
|
+
end
|
317
|
+
return [[x_pos, @y], distance, overshoot, turn] if overshoot >= 0
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
321
|
+
def as_json
|
322
|
+
Serializable.as_json(self).merge!(
|
323
|
+
:class => self.class.to_s,
|
324
|
+
:registry_id => registry_id,
|
325
|
+
:position => [ self.x, self.y ],
|
326
|
+
:velocity => [ self.x_vel, self.y_vel ],
|
327
|
+
:angle => self.a,
|
328
|
+
:moving => self.moving?
|
329
|
+
)
|
330
|
+
end
|
331
|
+
|
332
|
+
def update_from_json(json)
|
333
|
+
new_x, new_y = json[:position]
|
334
|
+
new_x_vel, new_y_vel = json[:velocity]
|
335
|
+
new_angle = json[:angle]
|
336
|
+
new_moving = json[:moving]
|
337
|
+
|
338
|
+
warp(new_x, new_y, new_x_vel, new_y_vel, new_angle, new_moving)
|
339
|
+
self
|
340
|
+
end
|
341
|
+
|
342
|
+
def image_filename
|
343
|
+
raise "No image filename defined"
|
344
|
+
end
|
345
|
+
|
346
|
+
def draw_zorder; ZOrder::Objects end
|
347
|
+
|
348
|
+
def draw(window)
|
349
|
+
anim = window.animation[window.media(image_filename)]
|
350
|
+
img = anim[Gosu::milliseconds / 100 % anim.size]
|
351
|
+
# Entity's pixel_x/pixel_y is the location of the upper-left corner
|
352
|
+
# draw_rot wants us to specify the point around which rotation occurs
|
353
|
+
# That should be the center
|
354
|
+
img.draw_rot(
|
355
|
+
self.pixel_x + CELL_WIDTH_IN_PIXELS / 2,
|
356
|
+
self.pixel_y + CELL_WIDTH_IN_PIXELS / 2,
|
357
|
+
draw_zorder, self.a)
|
358
|
+
# 0.5, 0.5, # rotate around the center
|
359
|
+
# 1, 1, # scaling factor
|
360
|
+
# @color, # modify color
|
361
|
+
# :add) # draw additively
|
362
|
+
end
|
363
|
+
|
364
|
+
def to_s
|
365
|
+
"#{self.class} (#{registry_id_safe}) at #{x}x#{y}"
|
366
|
+
end
|
367
|
+
|
368
|
+
def all_state
|
369
|
+
[registry_id_safe, @x, @y, @a, @x_vel, @y_vel, @moving]
|
370
|
+
end
|
371
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'game_2d/entity/owned_entity'
|
2
|
+
|
3
|
+
class Entity
|
4
|
+
|
5
|
+
class Block < OwnedEntity
|
6
|
+
MAX_LEVEL = 5
|
7
|
+
HP_PER_LEVEL = 5
|
8
|
+
MAX_HP = MAX_LEVEL * HP_PER_LEVEL
|
9
|
+
|
10
|
+
attr_reader :hp
|
11
|
+
|
12
|
+
def hp=(p); @hp = [[p, MAX_HP].min, 0].max; end
|
13
|
+
|
14
|
+
def all_state; super.push(hp); end
|
15
|
+
def as_json; super.merge!(:hp => hp); end
|
16
|
+
|
17
|
+
def update_from_json(json)
|
18
|
+
self.hp = json[:hp] if json[:hp]
|
19
|
+
super
|
20
|
+
end
|
21
|
+
|
22
|
+
def should_fall?
|
23
|
+
return false if owner || !empty_underneath?
|
24
|
+
|
25
|
+
case level
|
26
|
+
when 0
|
27
|
+
true
|
28
|
+
when 1
|
29
|
+
empty_on_left? || empty_on_right?
|
30
|
+
when 2
|
31
|
+
empty_on_left? && empty_on_right?
|
32
|
+
when 3
|
33
|
+
empty_on_left? && empty_on_right? && empty_above?
|
34
|
+
when 4
|
35
|
+
false
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def update
|
40
|
+
if should_fall?
|
41
|
+
accelerate(0, 1)
|
42
|
+
else
|
43
|
+
self.x_vel = self.y_vel = 0
|
44
|
+
end
|
45
|
+
move
|
46
|
+
end
|
47
|
+
|
48
|
+
def transparent_to_me?(other)
|
49
|
+
super ||
|
50
|
+
(other.registry_id == owner_id) ||
|
51
|
+
(other.is_a?(Pellet) && other.owner_id == owner_id)
|
52
|
+
end
|
53
|
+
|
54
|
+
def harmed_by(other)
|
55
|
+
puts "#{self}: Ouch!"
|
56
|
+
self.hp -= 1
|
57
|
+
@space.doom(self) if hp <= 0
|
58
|
+
end
|
59
|
+
|
60
|
+
def destroy!
|
61
|
+
owner.disown_block if owner
|
62
|
+
end
|
63
|
+
|
64
|
+
def level; (hp - 1) / HP_PER_LEVEL; end
|
65
|
+
|
66
|
+
def level_name
|
67
|
+
%w(dirt brick cement steel unlikelium)[level]
|
68
|
+
end
|
69
|
+
|
70
|
+
def image_filename; "#{level_name}.gif"; end
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|