adventure_rl 0.0.1.pre.ld42

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.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +12 -0
  3. data/.travis.yml +5 -0
  4. data/Gemfile +12 -0
  5. data/Gemfile.lock +47 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +31 -0
  8. data/Rakefile +11 -0
  9. data/adventure_rl.gemspec +48 -0
  10. data/bin/console +7 -0
  11. data/bin/mkaudio +196 -0
  12. data/bin/mkclip +223 -0
  13. data/bin/rdoc +9 -0
  14. data/bin/setup +8 -0
  15. data/bin/vimall +5 -0
  16. data/doc/Mask.md +183 -0
  17. data/doc/Point.md +95 -0
  18. data/doc/Window.md +139 -0
  19. data/lib/AdventureRL/Animation.rb +63 -0
  20. data/lib/AdventureRL/Audio.rb +75 -0
  21. data/lib/AdventureRL/AudioPlayer.rb +65 -0
  22. data/lib/AdventureRL/Button.rb +51 -0
  23. data/lib/AdventureRL/Clip.rb +91 -0
  24. data/lib/AdventureRL/ClipPlayer.rb +187 -0
  25. data/lib/AdventureRL/Deltatime.rb +51 -0
  26. data/lib/AdventureRL/EventHandlers/Buttons.rb +225 -0
  27. data/lib/AdventureRL/EventHandlers/EventHandler.rb +62 -0
  28. data/lib/AdventureRL/EventHandlers/MouseButtons.rb +142 -0
  29. data/lib/AdventureRL/Events/Event.rb +69 -0
  30. data/lib/AdventureRL/Events/Mouse.rb +60 -0
  31. data/lib/AdventureRL/FileGroup.rb +100 -0
  32. data/lib/AdventureRL/FileGroupPlayer.rb +226 -0
  33. data/lib/AdventureRL/Helpers/Error.rb +68 -0
  34. data/lib/AdventureRL/Helpers/MethodHelper.rb +20 -0
  35. data/lib/AdventureRL/Helpers/PipeMethods.rb +26 -0
  36. data/lib/AdventureRL/Image.rb +77 -0
  37. data/lib/AdventureRL/Layer.rb +273 -0
  38. data/lib/AdventureRL/Mask.rb +462 -0
  39. data/lib/AdventureRL/Menu.rb +92 -0
  40. data/lib/AdventureRL/Modifiers/Gravity.rb +60 -0
  41. data/lib/AdventureRL/Modifiers/Inventory.rb +104 -0
  42. data/lib/AdventureRL/Modifiers/Pusher.rb +61 -0
  43. data/lib/AdventureRL/Modifiers/Solid.rb +302 -0
  44. data/lib/AdventureRL/Modifiers/Velocity.rb +163 -0
  45. data/lib/AdventureRL/Point.rb +188 -0
  46. data/lib/AdventureRL/Quadtree.rb +237 -0
  47. data/lib/AdventureRL/Rectangle.rb +62 -0
  48. data/lib/AdventureRL/Settings.rb +80 -0
  49. data/lib/AdventureRL/SolidsManager.rb +170 -0
  50. data/lib/AdventureRL/Textbox.rb +195 -0
  51. data/lib/AdventureRL/TimingHandler.rb +225 -0
  52. data/lib/AdventureRL/Window.rb +152 -0
  53. data/lib/AdventureRL/misc/extensions.rb +80 -0
  54. data/lib/AdventureRL/misc/require_files.rb +45 -0
  55. data/lib/AdventureRL/version.rb +3 -0
  56. data/lib/adventure_rl.rb +22 -0
  57. data/lib/default_settings.yml +20 -0
  58. data/vimrc +4 -0
  59. metadata +237 -0
@@ -0,0 +1,462 @@
1
+ module AdventureRL
2
+ # The Mask is basically a bounding box or rectangle.
3
+ # It has a position (Point) and a size.
4
+ class Mask < Point
5
+ include Helpers::MethodHelper
6
+
7
+ # This array will be filled with any created Masks.
8
+ # Just so they won't get garbage collected
9
+ # <em>(not sure how garbage collection works)</em>.
10
+ MASKS = []
11
+
12
+ # Default settings for Mask.
13
+ # Are superseded by settings passed to <tt>#initialize</tt>.
14
+ DEFAULT_SETTINGS = Settings.new({
15
+ position: {
16
+ x: 0,
17
+ y: 0
18
+ },
19
+ size: {
20
+ width: 64,
21
+ height: 64
22
+ },
23
+ origin: {
24
+ x: :left,
25
+ y: :top
26
+ },
27
+ assign_to: nil
28
+ })
29
+
30
+ # Pass settings Hash or <tt>AdventureRL::Settings</tt> as argument.
31
+ # Supersedes <tt>DEFAULT_SETTINGS</tt>.
32
+ def initialize settings_arg = {}
33
+ MASKS << self
34
+ settings = DEFAULT_SETTINGS.merge settings_arg
35
+ super *settings.get(:position).values
36
+ @size = settings.get(:size)
37
+ @origin = settings.get(:origin)
38
+ @assigned_to = []
39
+ assign_to settings.get(:assign_to) if (settings.get(:assign_to))
40
+ @layer = nil
41
+ call_setup_method settings_arg
42
+ end
43
+
44
+ # Assign this Mask to an instance.
45
+ # This will make all Mask methods available as
46
+ # a fallback on the instance itself.
47
+ # This also gives the possibility to define event methods
48
+ # on the <tt>object</tt>.
49
+ def assign_to object
50
+ Helpers::PipeMethods.pipe_methods_from object, to: self
51
+ @assigned_to << object
52
+ end
53
+
54
+ # Returns all objects this Mask was assigned to.
55
+ def get_assigned
56
+ return @assigned_to
57
+ end
58
+
59
+ # Returns true if the Mask has been
60
+ # assigned to the passed <tt>object</tt>.
61
+ def assigned_to? object
62
+ return @assigned_to.include? object
63
+ end
64
+
65
+ # Returns self.
66
+ # Used to get an instance's mask when it has been assigned to it.
67
+ def get_mask
68
+ return self
69
+ end
70
+
71
+ # Returns <tt>true</tt>.
72
+ # Used to verify if an instance has a Mask assigned to it.
73
+ def has_mask?
74
+ return true
75
+ end
76
+
77
+ # Returns the size of the Mask.
78
+ # Can pass an optional <tt>target</tt> argument,
79
+ # which can be either an axis (<tt>:width</tt> or <tt>:height</tt>),
80
+ # or <tt>:all</tt>, which returns a Hash with both values.
81
+ def get_size target = :all
82
+ target = target.to_sym
83
+ return @size if (target == :all)
84
+ return @size[target] if (@size.key?(target))
85
+ return nil
86
+ end
87
+
88
+ # Set a new Mask size.
89
+ # <tt>args</tt> are very similar to Point#set_position,
90
+ # only that the axes (<tt>:x</tt> and <tt>:y</tt>) are replaced with
91
+ # <tt>:width</tt> and <tt>:height</tt>.
92
+ def set_size *args
93
+ new_size = parse_size(*args)
94
+ @size[:width] = new_size[:width] if (new_size.key? :width)
95
+ @size[:height] = new_size[:height] if (new_size.key? :height)
96
+ end
97
+ alias_method :resize, :set_size
98
+
99
+ # Returns the set origin.
100
+ # Can pass an optional <tt>target</tt> argument,
101
+ # which can be either an axis (<tt>:x</tt> or <tt>:y</tt>),
102
+ # or <tt>:all</tt>, which returns a Hash with both values.
103
+ def get_origin target = :all
104
+ target = target.to_sym
105
+ return @origin if (target == :all)
106
+ return @origin[target] if (@origin.key?(target))
107
+ return nil
108
+ end
109
+
110
+ # Returns the position Integer of a specific side.
111
+ # Takes one mandatory argument, <tt>side</tt>,
112
+ # which can be one of the following:
113
+ # <tt>:left</tt> or <tt>:right</tt>::
114
+ # Returns the x position of the left or right side/border, respectively.
115
+ # <tt>:top</tt> or <tt>:bottom</tt>::
116
+ # Returns the y position of the top or bottom side/border, respectively.
117
+ def get_side side
118
+ side = side.to_sym
119
+ case side
120
+ when :left
121
+ return get_side_left
122
+ when :right
123
+ return get_side_right
124
+ when :top
125
+ return get_side_top
126
+ when :bottom
127
+ return get_side_bottom
128
+ else
129
+ return nil
130
+ end
131
+ end
132
+
133
+ # Returns the real window position of <tt>side</tt>.
134
+ # See #get_side for usage.
135
+ def get_real_side side
136
+ axis = :x if ([:left, :right].include? side)
137
+ axis = :y if ([:top, :bottom].include? side)
138
+ side_pos = get_side(side) * get_scale(axis)
139
+ return side_pos unless (has_layer?)
140
+ case side
141
+ when :left, :right
142
+ return get_layer.get_real_side(:left) + side_pos
143
+ when :top, :bottom
144
+ return get_layer.get_real_side(:top) + side_pos
145
+ else
146
+ return nil
147
+ end
148
+ end
149
+
150
+ # Returns the positions of all four sides.
151
+ def get_sides
152
+ return {
153
+ left: get_side(:left),
154
+ right: get_side(:right),
155
+ top: get_side(:top),
156
+ bottom: get_side(:bottom)
157
+ }
158
+ end
159
+
160
+ # Returns the real window positions of all four sides.
161
+ def get_real_sides
162
+ return {
163
+ left: get_real_side(:left),
164
+ right: get_real_side(:right),
165
+ top: get_real_side(:top),
166
+ bottom: get_real_side(:bottom)
167
+ }
168
+ end
169
+
170
+ # Returns a Point with the position of a specific corner.
171
+ # Takes two mandatory arguments:
172
+ # <tt>side_x</tt>:: Either <tt>:left</tt> or <tt>:right</tt>.
173
+ # <tt>side_y</tt>:: Either <tt>:top</tt> or <tt>:bottom</tt>.
174
+ def get_corner side_x, side_y
175
+ side_x = side_x.to_sym
176
+ side_y = side_y.to_sym
177
+ return Point.new(
178
+ get_side(side_x),
179
+ get_side(side_y)
180
+ ) unless ([side_x, side_y].include? :center)
181
+ if (side_x == side_y)
182
+ center = get_center.values
183
+ return Point.new(*center)
184
+ elsif (side_x == :center)
185
+ return Point.new(
186
+ get_center(:x),
187
+ get_side(side_y)
188
+ )
189
+ elsif (side_y == :center)
190
+ return Point.new(
191
+ get_side(side_x),
192
+ get_center(:y)
193
+ )
194
+ end
195
+ return nil
196
+ end
197
+
198
+ # Returns the real window position of the corner.
199
+ # See #get_corner for usage.
200
+ def get_real_corner side_x, side_y
201
+ side_x = side_x.to_sym
202
+ side_y = side_y.to_sym
203
+ return Point.new(
204
+ get_real_side(side_x),
205
+ get_real_side(side_y)
206
+ ) unless ([side_x, side_y].include? :center)
207
+ if (side_x == side_y)
208
+ center = get_real_center.values
209
+ return Point.new(*center)
210
+ elsif (side_x == :center)
211
+ return Point.new(
212
+ get_real_center(:x),
213
+ get_real_side(side_y)
214
+ )
215
+ elsif (side_y == :center)
216
+ return Point.new(
217
+ get_real_side(side_x),
218
+ get_real_center(:y)
219
+ )
220
+ end
221
+ return nil
222
+ end
223
+
224
+ # Returns the center Point of the Mask.
225
+ # An optional <tt>target</tt> argument can be passed,
226
+ # which can either be an axis (<tt>:x</tt> or <tt>:y</tt>),
227
+ # or <tt>:all</tt>, which returns a Hash with both values.
228
+ def get_center target = :all
229
+ target = target.to_sym
230
+ return Point.new(
231
+ get_center_x,
232
+ get_center_y
233
+ ) if (target == :all)
234
+ return method("get_center_#{target.to_s}".to_sym).call if (get_point.keys.include? target)
235
+ return nil
236
+ end
237
+
238
+ # Returns the real window position of this center.
239
+ # See #get_center for usage.
240
+ def get_real_center target = :all
241
+ scale = get_scale
242
+ side = :left if (target == :x)
243
+ side = :top if (target == :y)
244
+ return Point.new(
245
+ (get_real_side(:left) + (get_center_x * scale[:x])),
246
+ (get_real_side(:top) + (get_center_y * scale[:y]))
247
+ ) if (target == :all)
248
+ return (
249
+ get_real_side(side) + (method("get_center_#{target.to_s}".to_sym).call * scale[target])
250
+ ) if (get_point.keys.include? target)
251
+ return nil
252
+ end
253
+
254
+ # Returns true if this Mask collides with <tt>other</tt> ...
255
+ # - Mask,
256
+ # - Point,
257
+ # - or Hash with keys <tt>:x</tt> and <tt>:y</tt>.
258
+ def collides_with? other
259
+ return collides_with_mask? other if (other.has_mask? rescue false)
260
+ return collides_with_point? other if (other.has_point? rescue false)
261
+ return collides_with_hash? other if (other.is_a?(Hash))
262
+ end
263
+
264
+ # Returns true if this Mask collides with <tt>other</tt> Mask.
265
+ def collides_with_mask? mask, checked = false
266
+ this_sides = get_real_sides
267
+ other_sides = mask.get_real_sides
268
+ return (
269
+ (
270
+ (
271
+ other_sides[:left] >= this_sides[:left] &&
272
+ other_sides[:left] <= this_sides[:right]
273
+ ) || (
274
+ other_sides[:right] >= this_sides[:left] &&
275
+ other_sides[:right] <= this_sides[:right]
276
+ ) || (
277
+ other_sides[:left] <= this_sides[:left] &&
278
+ other_sides[:right] >= this_sides[:right]
279
+ )
280
+ ) && (
281
+ (
282
+ other_sides[:top] >= this_sides[:top] &&
283
+ other_sides[:top] <= this_sides[:bottom]
284
+ ) || (
285
+ other_sides[:bottom] >= this_sides[:top] &&
286
+ other_sides[:bottom] <= this_sides[:bottom]
287
+ ) || (
288
+ other_sides[:top] <= this_sides[:top] &&
289
+ other_sides[:bottom] >= this_sides[:bottom]
290
+ )
291
+ )
292
+ ) || (!checked && mask.collides_with_mask?(self, true))
293
+ end
294
+
295
+ # Returns true if this Mask collides with <tt>other</tt> Point.
296
+ def collides_with_point? point
297
+ real_point = point.get_real_point
298
+ real_sides = get_real_sides
299
+ return (
300
+ real_point.x >= real_sides[:left] &&
301
+ real_point.x <= real_sides[:right] &&
302
+ real_point.y >= real_sides[:top] &&
303
+ real_point.y <= real_sides[:bottom]
304
+ )
305
+ end
306
+
307
+ # Returns true if this Mask collides with <tt>other</tt> Hash.
308
+ def collides_with_hash? hash
309
+ if (hash.keys.include_all?(:x, :y))
310
+ point = Point.new hash[:x], hash[:y]
311
+ return collides_with_point? point
312
+ end
313
+ return nil
314
+ end
315
+
316
+ # Set the parent Layer.
317
+ def set_layer layer
318
+ error(
319
+ "Passed argument `layer' must be an instance of `Layer', but got",
320
+ "`#{layer.inspect}:#{layer.class.name}'."
321
+ ) unless (layer.is_a? Layer)
322
+ @layer = layer
323
+ end
324
+
325
+ # Returns the parent Layer.
326
+ def get_layer
327
+ return @layer
328
+ end
329
+
330
+ # Returns true if this Mask has a parent Layer.
331
+ def has_layer?
332
+ return !!@layer
333
+ end
334
+
335
+ private
336
+
337
+ def call_setup_method args
338
+ return unless (method_exists?(:setup))
339
+ if (method_takes_arguments?(:setup))
340
+ setup args
341
+ else
342
+ setup
343
+ end
344
+ end
345
+
346
+ def call_method_on_assigned method_name, *args
347
+ get_assigned.each do |assigned_to|
348
+ meth = nil
349
+ meth = assigned_to.method(method_name) if (assigned_to.methods.include?(method_name))
350
+ meth.call(*args) if (meth)
351
+ end
352
+ end
353
+
354
+ def parse_size *args
355
+ size = {}
356
+ case args.size
357
+ when 2
358
+ size[:width] = args[0]
359
+ size[:height] = args[1]
360
+ when 1
361
+ Helpers::Error.error(
362
+ "Ambiguous argument `#{args[0]}' for Mask##{__method__}"
363
+ ) unless (args[0].is_a?(Hash))
364
+ Helpers::Error.error(
365
+ "Hash must include either :width, :height, or both keys for Mask##{__method__}"
366
+ ) unless (args[0].keys.include_any?(:width, :height))
367
+ size[:width] = args[0][:width] if (args[0][:width])
368
+ size[:height] = args[0][:height] if (args[0][:height])
369
+ else
370
+ Helpers::Error.error(
371
+ "Invalid amount of arguments for Mask##{__method__}.",
372
+ "Pass either two arguments representing the width and height axes, respectively, or",
373
+ "pass a single hash with the keys :width and :height with their respective axes values."
374
+ )
375
+ end
376
+ return size
377
+ end
378
+
379
+ # Returns this Masks Layer scale, if it has one.
380
+ def get_scale target = :all
381
+ return get_layer.get_real_scale target if (has_layer?)
382
+ scale = { x: 1.0, y: 1.0 }
383
+ return scale[target] if (scale.key? target)
384
+ return scale if (target == :all)
385
+ return nil
386
+ end
387
+
388
+ def get_side_left
389
+ case get_origin(:x)
390
+ when :left
391
+ return get_position(:x)
392
+ when :right
393
+ return get_position(:x) - get_size(:width)
394
+ when :center
395
+ return get_position(:x) - (get_size(:width) * 0.5)
396
+ else
397
+ return nil
398
+ end
399
+ end
400
+
401
+ def get_side_right
402
+ case get_origin(:x)
403
+ when :left
404
+ return get_position(:x) + get_size(:width)
405
+ when :right
406
+ return get_position(:x)
407
+ when :center
408
+ return get_position(:x) + (get_size(:width) * 0.5)
409
+ else
410
+ return nil
411
+ end
412
+ end
413
+
414
+ def get_side_top
415
+ case get_origin(:y)
416
+ when :top
417
+ return get_position(:y)
418
+ when :bottom
419
+ return get_position(:y) - get_size(:height)
420
+ when :center
421
+ return get_position(:y) - (get_size(:height) * 0.5)
422
+ else
423
+ return nil
424
+ end
425
+ end
426
+
427
+ def get_side_bottom
428
+ case get_origin(:y)
429
+ when :top
430
+ return get_position(:y) + get_size(:height)
431
+ when :bottom
432
+ return get_position(:y)
433
+ when :center
434
+ return get_position(:y) + (get_size(:height) * 0.5)
435
+ else
436
+ return nil
437
+ end
438
+ end
439
+
440
+ def get_center_x
441
+ case get_origin(:x)
442
+ when :left
443
+ return get_position(:x) + (get_size(:width) * 0.5)
444
+ when :right
445
+ return get_position(:x) - (get_size(:width) * 0.5)
446
+ when :center
447
+ return get_position(:x)
448
+ end
449
+ end
450
+
451
+ def get_center_y
452
+ case get_origin(:y)
453
+ when :top
454
+ return get_position(:y) + (get_size(:height) * 0.5)
455
+ when :bottom
456
+ return get_position(:y) - (get_size(:height) * 0.5)
457
+ when :center
458
+ return get_position(:y)
459
+ end
460
+ end
461
+ end
462
+ end
@@ -0,0 +1,92 @@
1
+ module AdventureRL
2
+ class Menu < Layer
3
+ # This Array will be filled with creaed Menus.
4
+ MENUS = []
5
+
6
+ def self.button_down btnid
7
+ get_active_menus.each do |menu|
8
+ menu.button_down btnid
9
+ end
10
+ end
11
+ def self.button_up btnid
12
+ get_active_menus.each do |menu|
13
+ menu.button_up btnid
14
+ end
15
+ end
16
+ def self.update
17
+ get_active_menus.each(&:update)
18
+ end
19
+ def self.get_active_menus
20
+ return MENUS.select(&:is_active?)
21
+ end
22
+
23
+ DEFAULT_SETTINGS = Settings.new(
24
+ active: false,
25
+ auto_update: false,
26
+ mouse_buttons_event_handler: {
27
+ auto_update: false,
28
+ only_mouse_buttons: true
29
+ }
30
+ )
31
+
32
+ def initialize settings = {}
33
+ @settings = DEFAULT_SETTINGS.merge settings
34
+ @mouse_buttons_event_handler = EventHandlers::MouseButtons.new @settings.get(:mouse_buttons_event_handler)
35
+ super @settings
36
+ @active = @settings.get :active
37
+ MENUS << self if (@settings.get(:auto_update))
38
+ end
39
+
40
+ # Overwrite #add_object method, so we can
41
+ # validate, that the given <tt>object</tt> is a Button.
42
+ def add_object object, id = DEFAULT_INVENTORY_ID
43
+ #Helpers::Error.error(
44
+ # "Expected given object to be a Button, but got",
45
+ # "'#{object.inspect}:#{object.class.name}`."
46
+ #) unless (object.is_a? Button)
47
+ super
48
+ @mouse_buttons_event_handler.subscribe object if (object.is_a? Button)
49
+ end
50
+ alias_method :add_button, :add_object
51
+ alias_method :add_item, :add_object
52
+ alias_method :add, :add_object
53
+ alias_method :<<, :add_object
54
+
55
+ def activate
56
+ @active = true
57
+ end
58
+
59
+ def deactivate
60
+ @active = false
61
+ end
62
+
63
+ def is_active?
64
+ return !!@active
65
+ end
66
+
67
+ def is_inactive?
68
+ return !is_active?
69
+ end
70
+
71
+ def button_down btnid
72
+ return if (is_inactive?)
73
+ @mouse_buttons_event_handler.button_down btnid
74
+ end
75
+
76
+ def button_up btnid
77
+ return if (is_inactive?)
78
+ @mouse_buttons_event_handler.button_up btnid
79
+ end
80
+
81
+ def update
82
+ return if (is_inactive?)
83
+ @mouse_buttons_event_handler.update
84
+ super
85
+ end
86
+
87
+ def draw
88
+ return if (is_inactive?)
89
+ super
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,60 @@
1
+ module AdventureRL
2
+ module Modifiers
3
+ module Gravity
4
+ #include AdventureRL::Modifiers::Velocity # NOTE: This modifier relies on Modifiers::Velocity
5
+
6
+ DEFAULT_GRAVITY_SETTINGS = Settings.new(
7
+ gravity_force: 1000.0,
8
+ gravity_direction: {
9
+ x: 0.0,
10
+ y: 1.0
11
+ }
12
+ )
13
+
14
+ def initialize settings = {}
15
+ @settings = DEFAULT_GRAVITY_SETTINGS.merge settings
16
+ @gravity = 0.0
17
+ @gravity_force = @settings.get :gravity_force
18
+ @gravity_direction = @settings.get :gravity_direction
19
+ #@max_velocity = @max_velocity_original.dup
20
+ super @settings
21
+ @max_velocity_original[:y] = Float::INFINITY
22
+ @max_velocity[:y] = @max_velocity_original[:y].dup
23
+ @velocity_decay[:y] = 0
24
+ end
25
+
26
+ # Apply gravitational force.
27
+ def gravitize
28
+ previous_position = get_position.dup
29
+ get_gravity_directions.each do |axis, multiplier|
30
+ next if (@has_increased_velocity_for[axis])
31
+ set_position axis => (get_position(axis) + multiplier) unless (multiplier == 0)
32
+ if (in_collision?)
33
+ @velocity[axis] = 0.0
34
+ else #if ([0, @gravity_direction[axis].sign].include? get_velocity(axis).sign)
35
+ increase_velocity_by(
36
+ axis => ((@gravity_force * @gravity_direction[axis]) * @velocity_deltatime.dt),
37
+ no_quick_turn_around: true
38
+ )
39
+ end
40
+ set_position previous_position
41
+ end
42
+ end
43
+
44
+ # Overwrite Modifiers::Velocity#move,
45
+ # so we can update the gravity.
46
+ def move
47
+ super
48
+ gravitize
49
+ end
50
+
51
+ private
52
+
53
+ def get_gravity_directions
54
+ return @gravity_direction.select do |axis, value|
55
+ next value != 0
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end