metro-ld26 0.3.4

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 (185) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +17 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +6 -0
  5. data/Gemfile +14 -0
  6. data/Guardfile +4 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +177 -0
  9. data/Rakefile +18 -0
  10. data/bin/metro +16 -0
  11. data/changelog.md +153 -0
  12. data/lib/assets/menu-movement.wav +0 -0
  13. data/lib/assets/menu-selection.wav +0 -0
  14. data/lib/assets/missing.ogg +0 -0
  15. data/lib/assets/missing.png +0 -0
  16. data/lib/assets/missing.wav +0 -0
  17. data/lib/assets/missing_animation.png +0 -0
  18. data/lib/commands/generate_game.rb +13 -0
  19. data/lib/commands/generate_model.rb +25 -0
  20. data/lib/commands/generate_scene.rb +36 -0
  21. data/lib/commands/generate_view.rb +21 -0
  22. data/lib/commands/thor.rb +83 -0
  23. data/lib/core_ext/class.rb +14 -0
  24. data/lib/core_ext/numeric.rb +59 -0
  25. data/lib/gosu_ext/color.rb +62 -0
  26. data/lib/gosu_ext/gosu_constants.rb +53 -0
  27. data/lib/locale/en.yml +35 -0
  28. data/lib/locale/locale.rb +1 -0
  29. data/lib/metro.rb +144 -0
  30. data/lib/metro/animation.rb +135 -0
  31. data/lib/metro/animation/after_interval_factory.rb +12 -0
  32. data/lib/metro/animation/animation_factory.rb +15 -0
  33. data/lib/metro/animation/easing/ease_in.rb +15 -0
  34. data/lib/metro/animation/easing/easing.rb +51 -0
  35. data/lib/metro/animation/easing/linear.rb +15 -0
  36. data/lib/metro/animation/has_animations.rb +70 -0
  37. data/lib/metro/animation/implicit_animation.rb +100 -0
  38. data/lib/metro/animation/on_update_operation.rb +96 -0
  39. data/lib/metro/animation/scene_animation.rb +16 -0
  40. data/lib/metro/asset_path.rb +97 -0
  41. data/lib/metro/events/control_definition.rb +11 -0
  42. data/lib/metro/events/controls.rb +42 -0
  43. data/lib/metro/events/event_data.rb +60 -0
  44. data/lib/metro/events/event_dictionary.rb +52 -0
  45. data/lib/metro/events/event_factory.rb +17 -0
  46. data/lib/metro/events/event_relay.rb +342 -0
  47. data/lib/metro/events/event_state_manager.rb +70 -0
  48. data/lib/metro/events/events.rb +3 -0
  49. data/lib/metro/events/has_events.rb +108 -0
  50. data/lib/metro/events/hit_list.rb +75 -0
  51. data/lib/metro/events/unknown_sender.rb +5 -0
  52. data/lib/metro/font.rb +69 -0
  53. data/lib/metro/game.rb +102 -0
  54. data/lib/metro/game/dsl.rb +68 -0
  55. data/lib/metro/image.rb +75 -0
  56. data/lib/metro/logging.rb +33 -0
  57. data/lib/metro/missing_scene.rb +21 -0
  58. data/lib/metro/models/audio/song.rb +33 -0
  59. data/lib/metro/models/draws.rb +86 -0
  60. data/lib/metro/models/key_value_coding.rb +38 -0
  61. data/lib/metro/models/model.rb +246 -0
  62. data/lib/metro/models/model_factory.rb +32 -0
  63. data/lib/metro/models/models.rb +62 -0
  64. data/lib/metro/models/properties/animation_property.rb +115 -0
  65. data/lib/metro/models/properties/array_property.rb +24 -0
  66. data/lib/metro/models/properties/boolean_property.rb +27 -0
  67. data/lib/metro/models/properties/color_property.rb +116 -0
  68. data/lib/metro/models/properties/dimensions_property.rb +84 -0
  69. data/lib/metro/models/properties/font_property.rb +130 -0
  70. data/lib/metro/models/properties/image_property.rb +96 -0
  71. data/lib/metro/models/properties/model_property.rb +84 -0
  72. data/lib/metro/models/properties/numeric_property.rb +29 -0
  73. data/lib/metro/models/properties/options_property/no_option.rb +29 -0
  74. data/lib/metro/models/properties/options_property/options.rb +98 -0
  75. data/lib/metro/models/properties/options_property/options_property.rb +125 -0
  76. data/lib/metro/models/properties/position_property.rb +90 -0
  77. data/lib/metro/models/properties/property.rb +221 -0
  78. data/lib/metro/models/properties/property_owner.rb +137 -0
  79. data/lib/metro/models/properties/sample_property.rb +84 -0
  80. data/lib/metro/models/properties/scale_property.rb +80 -0
  81. data/lib/metro/models/properties/song_property.rb +89 -0
  82. data/lib/metro/models/properties/text_property.rb +75 -0
  83. data/lib/metro/models/ui/animated_sprite.rb +85 -0
  84. data/lib/metro/models/ui/border.rb +95 -0
  85. data/lib/metro/models/ui/fps.rb +54 -0
  86. data/lib/metro/models/ui/generic.rb +66 -0
  87. data/lib/metro/models/ui/grid_drawer.rb +74 -0
  88. data/lib/metro/models/ui/image.rb +87 -0
  89. data/lib/metro/models/ui/label.rb +175 -0
  90. data/lib/metro/models/ui/menu.rb +214 -0
  91. data/lib/metro/models/ui/model_label.rb +65 -0
  92. data/lib/metro/models/ui/model_labeler.rb +79 -0
  93. data/lib/metro/models/ui/rectangle.rb +59 -0
  94. data/lib/metro/models/ui/sprite.rb +79 -0
  95. data/lib/metro/models/ui/tile_map.rb +132 -0
  96. data/lib/metro/models/ui/tmx/isometric_position.rb +43 -0
  97. data/lib/metro/models/ui/tmx/orthogonal_position.rb +15 -0
  98. data/lib/metro/models/ui/tmx/tile_layer.rb +78 -0
  99. data/lib/metro/models/ui/ui.rb +13 -0
  100. data/lib/metro/parameters/command_line_args_parser.rb +68 -0
  101. data/lib/metro/parameters/options.rb +25 -0
  102. data/lib/metro/parameters/parameters.rb +2 -0
  103. data/lib/metro/sample.rb +40 -0
  104. data/lib/metro/scene.rb +478 -0
  105. data/lib/metro/scenes.rb +154 -0
  106. data/lib/metro/song.rb +56 -0
  107. data/lib/metro/template_message.rb +60 -0
  108. data/lib/metro/transitions/edit_transition_scene.rb +100 -0
  109. data/lib/metro/transitions/fade_transition_scene.rb +66 -0
  110. data/lib/metro/transitions/scene_transitions.rb +44 -0
  111. data/lib/metro/transitions/transition_scene.rb +19 -0
  112. data/lib/metro/units/bounds.rb +8 -0
  113. data/lib/metro/units/calculation_validations.rb +74 -0
  114. data/lib/metro/units/dimensions.rb +60 -0
  115. data/lib/metro/units/point.rb +51 -0
  116. data/lib/metro/units/rectangle_bounds.rb +148 -0
  117. data/lib/metro/units/scale.rb +46 -0
  118. data/lib/metro/units/units.rb +6 -0
  119. data/lib/metro/version.rb +32 -0
  120. data/lib/metro/views/json_view.rb +60 -0
  121. data/lib/metro/views/no_view.rb +34 -0
  122. data/lib/metro/views/parsers.rb +42 -0
  123. data/lib/metro/views/scene_view.rb +107 -0
  124. data/lib/metro/views/view.rb +133 -0
  125. data/lib/metro/views/writers.rb +43 -0
  126. data/lib/metro/views/yaml_view.rb +94 -0
  127. data/lib/metro/window.rb +95 -0
  128. data/lib/setup_handlers/exit_if_dry_run.rb +26 -0
  129. data/lib/setup_handlers/game_execution.rb +65 -0
  130. data/lib/setup_handlers/load_game_configuration.rb +65 -0
  131. data/lib/setup_handlers/load_game_files.rb +101 -0
  132. data/lib/setup_handlers/move_to_game_directory.rb +25 -0
  133. data/lib/setup_handlers/reload_game_on_game_file_changes.rb +79 -0
  134. data/lib/templates/game/README.md.tt +43 -0
  135. data/lib/templates/game/assets/brand.jpg +0 -0
  136. data/lib/templates/game/assets/hero.png +0 -0
  137. data/lib/templates/game/lib/custom_easing.rb +32 -0
  138. data/lib/templates/game/metro.tt +63 -0
  139. data/lib/templates/game/models/hero.rb +62 -0
  140. data/lib/templates/game/scenes/brand_scene.rb +19 -0
  141. data/lib/templates/game/scenes/brand_to_title_scene.rb +13 -0
  142. data/lib/templates/game/scenes/first_scene.rb +28 -0
  143. data/lib/templates/game/scenes/game_scene.rb +43 -0
  144. data/lib/templates/game/scenes/title_scene.rb +15 -0
  145. data/lib/templates/game/views/brand.yaml +4 -0
  146. data/lib/templates/game/views/brand_to_title.yaml +8 -0
  147. data/lib/templates/game/views/first.yaml +26 -0
  148. data/lib/templates/game/views/title.yaml +11 -0
  149. data/lib/templates/message.erb +23 -0
  150. data/lib/templates/model.rb.tt +111 -0
  151. data/lib/templates/scene.rb.tt +140 -0
  152. data/lib/templates/view.yaml.tt +11 -0
  153. data/lib/tmx_ext/object.rb +26 -0
  154. data/lib/tmx_ext/tile_set.rb +41 -0
  155. data/metro.gemspec +57 -0
  156. data/metro.png +0 -0
  157. data/spec/core_ext/numeric_spec.rb +78 -0
  158. data/spec/core_ext/string_spec.rb +33 -0
  159. data/spec/gosu_ext/color_spec.rb +80 -0
  160. data/spec/metro/image_spec.rb +33 -0
  161. data/spec/metro/models/key_value_coding_spec.rb +61 -0
  162. data/spec/metro/models/properties/array_property_spec.rb +60 -0
  163. data/spec/metro/models/properties/color_property_spec.rb +85 -0
  164. data/spec/metro/models/properties/dimensions_spec.rb +29 -0
  165. data/spec/metro/models/properties/font_property_spec.rb +127 -0
  166. data/spec/metro/models/properties/numeric_property_spec.rb +46 -0
  167. data/spec/metro/models/properties/options_property/no_option_spec.rb +25 -0
  168. data/spec/metro/models/properties/options_property/options_property_spec.rb +133 -0
  169. data/spec/metro/models/properties/options_property/options_spec.rb +125 -0
  170. data/spec/metro/models/properties/position_property_spec.rb +90 -0
  171. data/spec/metro/models/ui/label_spec.rb +259 -0
  172. data/spec/metro/parameters/command_line_args_parser_spec.rb +42 -0
  173. data/spec/metro/scene_spec.rb +15 -0
  174. data/spec/metro/scene_views/json_view_spec.rb +27 -0
  175. data/spec/metro/scene_views/yaml_view_spec.rb +38 -0
  176. data/spec/metro/scenes_spec.rb +77 -0
  177. data/spec/metro/units/point_spec.rb +132 -0
  178. data/spec/metro/units/rectangle_bounds_spec.rb +56 -0
  179. data/spec/metro/views/view_spec.rb +53 -0
  180. data/spec/setup_handlers/exit_if_dry_run_spec.rb +27 -0
  181. data/spec/setup_handlers/reload_game_on_game_file_changes_spec.rb +68 -0
  182. data/spec/spec_helper.rb +20 -0
  183. data/spec/tmx_ext/object_spec.rb +96 -0
  184. data/spec/tmx_ext/tile_set_spec.rb +24 -0
  185. metadata +379 -0
@@ -0,0 +1,19 @@
1
+ module Metro
2
+ class TransitionScene < Scene
3
+
4
+ attr_accessor :next_scene, :previous_scene, :options
5
+
6
+ def prepare_transition_from(old_scene)
7
+ next_scene.prepare_transition_from(old_scene)
8
+ @previous_scene = old_scene
9
+ end
10
+
11
+ def prepare_transition_to(new_scene)
12
+ previous_scene.prepare_transition_to(new_scene)
13
+ end
14
+
15
+ end
16
+ end
17
+
18
+ require_relative 'fade_transition_scene'
19
+ require_relative 'edit_transition_scene'
@@ -0,0 +1,8 @@
1
+ module Metro
2
+ module Units
3
+
4
+ # Rectangle Bounds are common enough to be considered simply Bounds.
5
+ Bounds = RectangleBounds
6
+
7
+ end
8
+ end
@@ -0,0 +1,74 @@
1
+ module Metro
2
+ module Units
3
+ module CalculationValidations
4
+
5
+ #
6
+ # @param [Object] value the other object that needs to be validated.
7
+ #
8
+ def check_calculation_requirements(value)
9
+ if calculation_requirements.find { |method| ! value.respond_to?(method) }
10
+ raise "Unable to perform operation with #{value} #{value.class} It is missing a property #{calculation_requirements.join(",")}"
11
+ end
12
+ end
13
+
14
+ #
15
+ # @return [Array] an array of methods that are required to be on the
16
+ # object for it to be correctly calculated.
17
+ #
18
+ # @note this method is intended to be defined in the including class. This
19
+ # method is included here when one has not been provided.
20
+ def calculation_requirements
21
+ []
22
+ end
23
+
24
+ #
25
+ # Add this object to another object.
26
+ #
27
+ # @return a new object that is the sum of the two objects
28
+ #
29
+ def +(value)
30
+ self.class.new *calculate(value,:+)
31
+ end
32
+
33
+ #
34
+ # Subtract the other object from this object.
35
+ #
36
+ # @return a new object that is the difference of the original object
37
+ # and the value specified.
38
+ #
39
+ def -(value)
40
+ self.class.new *calculate(value,:-)
41
+ end
42
+
43
+ #
44
+ # Multiply this object and another object.
45
+ #
46
+ # @return a new object that is the product of the two objects.
47
+ #
48
+ def *(value)
49
+ self.class.new *calculate(value,:*)
50
+ end
51
+
52
+ #
53
+ # This generic method will perform the calculation defined by the
54
+ # operation for all the calculation requirements defined.
55
+ #
56
+ # @param [value] value this is the other value that is being added,
57
+ # subtracted, etc. to the current object.
58
+ # @param [Symbol] operation this is the mathematical operation that
59
+ # is being performed between all the calc requirements of the current
60
+ # object and other value.
61
+ #
62
+ # @return [Array] an array of reults from the calculations of all the
63
+ # requirements.
64
+ #
65
+ def calculate(value,operation)
66
+ check_calculation_requirements(value)
67
+ calculation_requirements.map do |requirement|
68
+ send(requirement).send(operation,value.send(requirement))
69
+ end
70
+ end
71
+
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,60 @@
1
+ module Metro
2
+ module Units
3
+
4
+ #
5
+ # Represents an object that contains both the width and height.
6
+ #
7
+ class Dimensions < Struct.new(:width,:height)
8
+ include CalculationValidations
9
+
10
+ #
11
+ # Create a dimensions objects with zero width and zero height.
12
+ #
13
+ def self.none
14
+ of 0.0, 0.0
15
+ end
16
+
17
+ #
18
+ # Parse a string representation of a dimensions object. The
19
+ # supported formated is a comma-delimited string that contains
20
+ # two attributes width and height.
21
+ #
22
+ def self.parse(string)
23
+ of *string.to_s.split(",",2).map(&:to_f)
24
+ end
25
+
26
+
27
+ #
28
+ # An alternate way of creating a dimensions object which
29
+ # will enforce that the values added are converted to floating
30
+ # point numbers.
31
+ #
32
+ def self.of(width=0.0,height=0.0)
33
+ new width.to_f, height.to_f
34
+ end
35
+
36
+ def to_s
37
+ "#{width},#{height}"
38
+ end
39
+
40
+ #
41
+ # Compare the dimension to another dimensions-like structure.
42
+ #
43
+ # @return [Fixnum] -1 if the dimensions is smaller than the other dimension,
44
+ # 0 if the dimensions are exactly the same, 1 if the dimensions are bigger
45
+ # then the other dimensions.
46
+ #
47
+ def <=>(value)
48
+ check_calculation_requirements(value)
49
+ (width * height) <=> (value.width * value.height)
50
+ end
51
+
52
+ private
53
+
54
+ def calculation_requirements
55
+ [ :width, :height ]
56
+ end
57
+
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,51 @@
1
+ module Metro
2
+ module Units
3
+
4
+ #
5
+ # Represents and object that contains the x, y, and z position.
6
+ #
7
+ class Point < Struct.new(:x,:y,:z)
8
+ include CalculationValidations
9
+
10
+ #
11
+ # Generate a point at 0,0,0.
12
+ #
13
+ def self.zero
14
+ new 0.0, 0.0, 0.0
15
+ end
16
+
17
+ #
18
+ # An alternate way of creating a point. Creation here converts
19
+ # all inputs to floating point and assumes that the z-value is
20
+ # zero (as this is a 2D universe).
21
+ #
22
+ def self.at(x=0.0,y=0.0,z=0.0)
23
+ new x.to_f, y.to_f, z.to_f
24
+ end
25
+
26
+ #
27
+ # Parse a string representation of a point object. The
28
+ # supported formated is a comma-delimited string that contains
29
+ # either "x,y" or "x,y,z".
30
+ #
31
+ def self.parse(string)
32
+ at *string.to_s.split(",",3).map(&:to_f)
33
+ end
34
+
35
+ # As this is a 2D world, the Z is often refered to as a the z-ordering
36
+ alias_method :z_order, :z
37
+ alias_method :z_order=, :z=
38
+
39
+ def to_s
40
+ "#{x},#{y},#{z}"
41
+ end
42
+
43
+ private
44
+
45
+ def calculation_requirements
46
+ [ :x, :y, :z ]
47
+ end
48
+
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,148 @@
1
+ module Metro
2
+ module Units
3
+
4
+ #
5
+ # An object that represents a rectanglar bounds.
6
+ #
7
+ class RectangleBounds
8
+ include CalculationValidations
9
+
10
+ attr_accessor :left, :right, :top, :bottom
11
+
12
+ def self.none
13
+ new left: 0, right: 0, top: 0, bottom: 0
14
+ end
15
+
16
+ #
17
+ # Create a bounds with bounds.
18
+ #
19
+ def initialize(params = {})
20
+ @left = params[:left].to_f
21
+ @top = params[:top].to_f
22
+ @right = params[:right].to_f
23
+ @bottom = params[:bottom].to_f
24
+ end
25
+
26
+ #
27
+ # Move the bounds by the amount specified in the point.
28
+ #
29
+ def shift(point)
30
+ self.left = self.left + point.x
31
+ self.right = self.right + point.x
32
+ self.top = self.top + point.y
33
+ self.bottom = self.bottom + point.y
34
+ end
35
+
36
+ #
37
+ # Create a new RectangleBounds that is larger than the
38
+ # current bounds, given the specifed hash of parameters
39
+ # that contain the amount to expand in the four directions
40
+ # left, right, top, bottom.
41
+ #
42
+ def enlarge(params = {})
43
+ self.class.new(left: left - params[:left].to_i,
44
+ right: right + params[:right].to_i,
45
+ top: top - params[:top].to_i,
46
+ bottom: bottom + params[:bottom].to_i)
47
+ end
48
+
49
+ def shrink(params = {})
50
+ self.class.new(left: left + params[:left].to_i,
51
+ right: right - params[:right].to_i,
52
+ top: top + params[:top].to_i,
53
+ bottom: bottom - params[:bottom].to_i)
54
+ end
55
+
56
+ def clamp(point)
57
+ return point if contains?(point)
58
+
59
+ new_point = Point.new(point.x,point.y)
60
+
61
+ if point.x < left
62
+ new_point.x = left
63
+ end
64
+
65
+ if point.x > right
66
+ new_point.x = right
67
+ end
68
+
69
+ if point.y < top
70
+ new_point.y = top
71
+ end
72
+
73
+ if point.y > bottom
74
+ new_point.y = bottom
75
+ end
76
+
77
+ new_point
78
+ end
79
+
80
+ def top_left
81
+ point_at left, top
82
+ end
83
+
84
+ def top_right
85
+ point_at right, top
86
+ end
87
+
88
+ def bottom_right
89
+ point_at right, bottom
90
+ end
91
+
92
+ def bottom_left
93
+ point_at left, bottom
94
+ end
95
+
96
+ def width
97
+ (right - left)
98
+ end
99
+
100
+ def height
101
+ (bottom - top)
102
+ end
103
+
104
+ def center
105
+ top_left + point_at(width/2,height/2)
106
+ end
107
+
108
+ def dimensions
109
+ Dimensions.of width, height
110
+ end
111
+
112
+ def ==(value)
113
+ check_calculation_requirements(value)
114
+ left == value.left and right == value.right and top == value.top and bottom == value.bottom
115
+ end
116
+
117
+ #
118
+ # Does this bounds contain the following point?
119
+ #
120
+ def contains?(point)
121
+ point.x > left and point.x < right and point.y > top and point.y < bottom
122
+ end
123
+
124
+ #
125
+ # Does this rectanglular bounds intersect with another rectanglular bounds?
126
+ #
127
+ def intersect?(b)
128
+ not(b.left > right or b.right < left or b.top > bottom or b.bottom < top)
129
+ end
130
+
131
+ def to_s
132
+ "(#{left},#{top}) to (#{right},#{bottom})"
133
+ end
134
+
135
+ private
136
+
137
+ def point_at(x,y)
138
+ Point.at(x,y)
139
+ end
140
+
141
+ def calculation_requirements
142
+ [ :left, :right, :top, :bottom ]
143
+ end
144
+
145
+ end
146
+
147
+ end
148
+ end
@@ -0,0 +1,46 @@
1
+ module Metro
2
+ module Units
3
+
4
+ #
5
+ # Represents an object that contains the x scale factor, and y scale factor.
6
+ #
7
+ class Scale < Struct.new(:x_factor,:y_factor)
8
+ include CalculationValidations
9
+
10
+ #
11
+ # Create a scale that is 1:1.
12
+ #
13
+ def self.one
14
+ new 1.0, 1.0
15
+ end
16
+
17
+ #
18
+ # An alternative way to create a scale which will automatically convert
19
+ # the inputs into floating numbers.
20
+ #
21
+ def self.to(x,y)
22
+ new x.to_f, y.to_f
23
+ end
24
+
25
+ #
26
+ # Parse a string representation of a scale object. The
27
+ # supported formated is a comma-delimited string that contains
28
+ # two attributes x-factor and y-factor.
29
+ #
30
+ def self.parse(string)
31
+ to *string.split(",",2).map(&:to_f)
32
+ end
33
+
34
+ def to_s
35
+ "#{x_factor},#{y_factor}"
36
+ end
37
+
38
+ private
39
+
40
+ def calculation_requirements
41
+ [ :x_factor, :y_factor ]
42
+ end
43
+
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,6 @@
1
+ require_relative 'calculation_validations'
2
+ require_relative 'point'
3
+ require_relative 'dimensions'
4
+ require_relative 'scale'
5
+ require_relative 'rectangle_bounds'
6
+ require_relative 'bounds'
@@ -0,0 +1,32 @@
1
+ module Metro
2
+ VERSION = "0.3.4"
3
+ WEBSITE = "https://github.com/burtlo/metro"
4
+ CONTACT_EMAILS = ["dev@rubymetro.com"]
5
+
6
+ def self.changes_for_version(version)
7
+
8
+ change = Struct::Changes.new(nil,[])
9
+
10
+ grab_changes = false
11
+
12
+ changelog_filename = "#{File.dirname(__FILE__)}/../../changelog.md"
13
+
14
+ File.open(changelog_filename,'r') do |file|
15
+ while (line = file.gets) do
16
+
17
+ if line =~ /^##\s*#{version.gsub('.','\.')}\s*\/\s*(.+)\s*$/
18
+ grab_changes = true
19
+ change.date = $1.strip
20
+ elsif line =~ /^##\s*.+$/
21
+ grab_changes = false
22
+ elsif grab_changes
23
+ change.changes.push line
24
+ end
25
+
26
+ end
27
+ end
28
+
29
+ change
30
+ end
31
+
32
+ end