metro 0.1.5 → 0.1.6

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 (63) hide show
  1. data/README.md +19 -0
  2. data/changelog.md +8 -0
  3. data/lib/assets/missing_animation.png +0 -0
  4. data/lib/commands/generate_model.rb +2 -2
  5. data/lib/commands/generate_scene.rb +12 -2
  6. data/lib/commands/generate_view.rb +1 -1
  7. data/lib/gosu_ext/color.rb +1 -1
  8. data/lib/gosu_ext/image.rb +5 -0
  9. data/lib/metro.rb +18 -5
  10. data/lib/metro/animation/animation.rb +31 -0
  11. data/lib/metro/asset_path.rb +9 -0
  12. data/lib/metro/events/event_dictionary.rb +53 -0
  13. data/lib/metro/events/event_factory.rb +5 -3
  14. data/lib/metro/events/has_events.rb +5 -6
  15. data/lib/metro/game.rb +1 -1
  16. data/lib/metro/models/dimensions.rb +21 -0
  17. data/lib/metro/models/model.rb +149 -52
  18. data/lib/metro/models/model_factory.rb +4 -5
  19. data/lib/metro/models/{generic.rb → models/generic.rb} +0 -0
  20. data/lib/metro/models/{grid_drawer.rb → models/grid_drawer.rb} +4 -9
  21. data/lib/metro/models/{image.rb → models/image.rb} +11 -14
  22. data/lib/metro/models/models/label.rb +44 -0
  23. data/lib/metro/models/{menu.rb → models/menu.rb} +23 -18
  24. data/lib/metro/models/{rectangle.rb → models/rectangle.rb} +6 -5
  25. data/lib/metro/models/point.rb +23 -0
  26. data/lib/metro/models/properties/angle.rb +43 -0
  27. data/lib/metro/models/properties/animation.rb +143 -0
  28. data/lib/metro/models/properties/color.rb +113 -0
  29. data/lib/metro/models/properties/dimensions.rb +66 -0
  30. data/lib/metro/models/properties/font.rb +155 -0
  31. data/lib/metro/models/properties/image.rb +101 -0
  32. data/lib/metro/models/properties/numeric.rb +29 -0
  33. data/lib/metro/models/properties/position.rb +84 -0
  34. data/lib/metro/models/properties/property.rb +111 -0
  35. data/lib/metro/models/properties/scale.rb +89 -0
  36. data/lib/metro/models/properties/text.rb +66 -0
  37. data/lib/metro/models/properties/velocity.rb +80 -0
  38. data/lib/metro/models/scale.rb +21 -0
  39. data/lib/metro/scene.rb +19 -1
  40. data/lib/metro/scenes.rb +91 -31
  41. data/lib/metro/transitions/scene_transitions.rb +8 -0
  42. data/lib/metro/version.rb +1 -1
  43. data/lib/metro/views/view.rb +9 -1
  44. data/lib/templates/game/metro.tt +1 -1
  45. data/lib/templates/game/models/game_model.rb +3 -0
  46. data/lib/templates/game/scenes/brand_scene.rb +1 -1
  47. data/lib/templates/game/scenes/brand_to_title_scene.rb +1 -1
  48. data/lib/templates/game/scenes/game_scene.rb +19 -0
  49. data/lib/templates/game/scenes/title_scene.rb +1 -1
  50. data/lib/templates/game/views/brand_to_title.yaml +2 -2
  51. data/lib/templates/game/views/title.yaml +3 -3
  52. data/lib/templates/{model.rb.erb → model.rb.tt} +1 -1
  53. data/lib/templates/{scene.rb.erb → scene.rb.tt} +1 -1
  54. data/lib/templates/view.yaml.tt +6 -0
  55. data/spec/metro/models/models/label_spec.rb +110 -0
  56. data/spec/metro/models/properties/color_spec.rb +85 -0
  57. data/spec/metro/models/properties/font_spec.rb +129 -0
  58. data/spec/metro/models/properties/numeric_property_spec.rb +46 -0
  59. data/spec/metro/models/properties/position_property_spec.rb +90 -0
  60. data/spec/metro/scenes_spec.rb +77 -0
  61. metadata +50 -16
  62. data/lib/metro/models/label.rb +0 -63
  63. data/lib/templates/view.yaml.erb +0 -32
data/README.md CHANGED
@@ -53,3 +53,22 @@ $ metro generate scene first
53
53
  ```
54
54
 
55
55
  This should generate a scene in the scenes directory. The scene file contains a lot of examples of how to draw, animate and have your scene listen to events.
56
+
57
+
58
+ ### Resources
59
+
60
+ #### Art
61
+
62
+ * [Lost Garden](http://www.lostgarden.com/2007/05/dancs-miraculously-flexible-game.html)
63
+
64
+ #### Books
65
+
66
+ * [Rules of Play](http://www.amazon.com/dp/0262240459)
67
+ * [Game Programming Gems 8](http://www.amazon.com/dp/1584507020)
68
+ * [Game Feel](http://www.amazon.com/dp/0123743281)
69
+ * [Game Coding Complete](http://www.amazon.com/dp/1584506806)
70
+ * [Game Design Workshop](http://www.amazon.com/dp/0240809742)
71
+ * [Challenges For Game Designers](http://www.amazon.com/dp/158450580X)
72
+ * [The Art of Game Design: A book of lenses](http://www.amazon.com/dp/0123694965)
73
+ * [A Theory of Fun](http://www.theoryoffun.com)
74
+ * [Andrew Rollings and Ernest Adams on Game Design](http://www.amazon.com/dp/1592730019)
@@ -1,5 +1,13 @@
1
1
  # Metro
2
2
 
3
+ ## 0.1.6 / 2012-11-07
4
+
5
+ * Events are shared from superclasses to subclases.
6
+ * Templates updated to use GameScene and GameModel for each game.
7
+ * Models are automatically added to the update loop
8
+ * Model properties now make it easier to store/retrieve various
9
+ common numeric, position font, image, and animation properties.
10
+
3
11
  ## 0.1.5 / 2012-11-01
4
12
 
5
13
  * Metro.reload! will reload all game classes
@@ -9,7 +9,7 @@ module Metro
9
9
  end
10
10
 
11
11
  def model_name
12
- name.classify
12
+ name.camelize
13
13
  end
14
14
 
15
15
  end
@@ -17,7 +17,7 @@ module Metro
17
17
  argument :name
18
18
 
19
19
  def create_model_file
20
- template "model.rb.erb", "models/#{model_filename}.rb"
20
+ template "model.rb.tt", "models/#{model_filename}.rb"
21
21
  end
22
22
 
23
23
  end
@@ -11,7 +11,12 @@ module Metro
11
11
 
12
12
  def scene_class_name
13
13
  scene_name = name.gsub(/_?Scene$/i,'')
14
- "#{scene_name.classify}Scene"
14
+ "#{scene_name.camelize}Scene"
15
+ end
16
+
17
+ def view_filename
18
+ view_name = name.to_s.gsub(/_?Scene$/i,'')
19
+ view_name.underscore
15
20
  end
16
21
 
17
22
  end
@@ -19,8 +24,13 @@ module Metro
19
24
  argument :name
20
25
 
21
26
  def create_scene_file
22
- template "scene.rb.erb", "scenes/#{scene_filename}.rb"
27
+ template "scene.rb.tt", "scenes/#{scene_filename}.rb"
23
28
  end
29
+
30
+ def create_view_file
31
+ template "view.yaml.tt", "views/#{view_filename}.yaml"
32
+ end
33
+
24
34
  end
25
35
 
26
36
  end
@@ -14,7 +14,7 @@ module Metro
14
14
  argument :name
15
15
 
16
16
  def create_view_file
17
- template "view.yaml.erb", "views/#{view_filename}.yaml"
17
+ template "view.yaml.tt", "views/#{view_filename}.yaml"
18
18
  end
19
19
  end
20
20
 
@@ -26,7 +26,7 @@ class Gosu::Color
26
26
  end
27
27
 
28
28
  def self.parse_rgba(rgba)
29
- if rgba =~ /rgba\(([\d]{1,3}),([\d]{1,3}),([\d]{1,3}),(\d(?:\.\d)?)\)/
29
+ if rgba =~ /rgba\(([\d]{1,3}(?:\.\d*)?),([\d]{1,3}(?:\.\d*)?),([\d]{1,3}(?:\.\d*)?),(\d(?:\.\d*)?)\)/
30
30
  [ (255 * $4.to_f).floor.to_i, $1.to_i, $2.to_i, $3.to_i ]
31
31
  end
32
32
  end
@@ -0,0 +1,5 @@
1
+ class Gosu::Image
2
+
3
+ attr_accessor :path, :tileable
4
+
5
+ end
@@ -1,10 +1,13 @@
1
1
  require 'gosu'
2
2
  require 'gosu_ext/color'
3
+ require 'gosu_ext/image'
3
4
  require 'gosu_ext/gosu_constants'
4
5
  require 'i18n'
5
6
  require 'active_support'
6
7
  require 'active_support/dependencies'
7
8
  require 'active_support/inflector'
9
+ require 'active_support/core_ext/hash'
10
+ require 'active_support/hash_with_indifferent_access'
8
11
 
9
12
  require 'core_ext/numeric'
10
13
  require 'logger'
@@ -12,6 +15,9 @@ require 'erb'
12
15
 
13
16
  require 'locale/locale'
14
17
  require 'metro/asset_path'
18
+ require 'metro/models/point'
19
+ require 'metro/models/scale'
20
+ require 'metro/models/dimensions'
15
21
  require 'metro/logging'
16
22
  require 'metro/version'
17
23
  require 'metro/template_message'
@@ -20,7 +26,6 @@ require 'metro/game'
20
26
  require 'metro/scene'
21
27
  require 'metro/scenes'
22
28
  require 'metro/models/model'
23
- require 'metro/models/generic'
24
29
 
25
30
  require_relative 'metro/missing_scene'
26
31
 
@@ -41,6 +46,10 @@ module Metro
41
46
  'metro'
42
47
  end
43
48
 
49
+ def asset_dir
50
+ File.join File.dirname(__FILE__), "assets"
51
+ end
52
+
44
53
  #
45
54
  # Run will load the contents of the game contents and game files
46
55
  # within the current working directory and start the game. By default
@@ -58,6 +67,7 @@ module Metro
58
67
  end
59
68
 
60
69
  def load_game_files!
70
+ EventDictionary.reset!
61
71
  prepare_watcher!
62
72
  load_game_files
63
73
  execute_watcher!
@@ -104,18 +114,21 @@ module Metro
104
114
 
105
115
  def load_game_files
106
116
  $LOAD_PATH.unshift(Dir.pwd) unless $LOAD_PATH.include?(Dir.pwd)
107
- load_paths 'models', 'scenes', 'lib'
117
+ load_paths 'lib'
118
+ load_path 'scenes', prioritize: 'game_scene.rb'
119
+ load_path 'models', prioritize: 'game_model.rb'
108
120
  end
109
121
 
110
122
  def load_paths(*paths)
111
123
  paths.flatten.compact.each {|path| load_path path }
112
124
  end
113
125
 
114
- def load_path(path)
115
- Dir["#{path}/**/*.rb"].each {|model| require_or_load model }
126
+ def load_path(path,options = {})
127
+ files = Dir["#{path}/**/*.rb"]
128
+ files.sort! {|file| File.basename(file) == options[:prioritize] ? -1 : 1 }
129
+ files.each {|model| require_or_load model }
116
130
  end
117
131
 
118
-
119
132
  def load_game_configuration(filename)
120
133
  gamefile = File.basename(filename)
121
134
  game_files_exist!(gamefile)
@@ -0,0 +1,31 @@
1
+ module Metro
2
+
3
+ #
4
+ # The animation is an wrapper object for an array of Gosu::Images that also contains
5
+ # the additional information on the path, height, width, and tileability.
6
+ #
7
+ class Animation
8
+
9
+ attr_accessor :images, :path, :height, :width, :tileable
10
+
11
+ def initialize(params = {})
12
+ @images = Array(params[:images])
13
+ @path = params[:path]
14
+ @height = params[:height]
15
+ @width = params[:width]
16
+ @tileable = params[:tileable]
17
+ end
18
+
19
+ def to_hash
20
+ { path: path, width: width.to_i, height: height.to_i, tileable: !!tileable }
21
+ end
22
+
23
+ #
24
+ # @return a Gosu::Image to be displayed in a animation sequence.
25
+ #
26
+ def image
27
+ images[Gosu::milliseconds / 100 % images.size]
28
+ end
29
+
30
+ end
31
+ end
@@ -20,3 +20,12 @@
20
20
  def asset_path(name)
21
21
  File.join Dir.pwd, "assets", name
22
22
  end
23
+
24
+
25
+ #
26
+ # The metro_asset_path is a helper which will generate a filepath based on the directory
27
+ # of the metro library. This is used to retrieve assets internally for missing images.
28
+ #
29
+ def metro_asset_path(name)
30
+ File.join Metro.asset_dir, name
31
+ end
@@ -0,0 +1,53 @@
1
+ require_relative 'event_factory'
2
+
3
+ module Metro
4
+
5
+ module EventDictionary
6
+ extend self
7
+
8
+ #
9
+ # All defined events within this dictionary.
10
+ #
11
+ def events
12
+ @events ||= Hash.new
13
+ end
14
+
15
+ #
16
+ # @example Adding a new SceneEvent to the Dictionary
17
+ #
18
+ # SceneEventDictionary.add target: scene_name, type: event_type, args: args, block: block
19
+ #
20
+ def add(params = {})
21
+ target = params[:target]
22
+ event = EventFactory.new params[:type], params[:args], &params[:block]
23
+ events[target] = events_for_target(target).push event
24
+ end
25
+
26
+ #
27
+ # Return all the events for all the specified targets.
28
+ #
29
+ def events_for_targets(*list)
30
+ found_events = Array(list).flatten.compact.map {|s| events_for_target(s) }.flatten.compact
31
+ log.debug "Retrieved (#{found_events.count}) events for [#{list.join(",")}]"
32
+ found_events
33
+ end
34
+
35
+ #
36
+ # Return the events for the specified target
37
+ #
38
+ def events_for_target(scene_name)
39
+ events[scene_name] ||= []
40
+ end
41
+
42
+ #
43
+ # When the game is reset the event dictionary needs to flush out all of the events that it
44
+ # has loaded as the game files will be reloaded. All metro related components will not
45
+ # be removed as those files are not reloaded when the game is reloaded.
46
+ #
47
+ def reset!
48
+ events.delete_if { |name,events| ! name.start_with? "metro/" }
49
+ end
50
+
51
+ end
52
+
53
+ end
@@ -2,11 +2,13 @@ module Metro
2
2
 
3
3
  class EventFactory
4
4
 
5
- attr_reader :event, :buttons, :block
5
+ attr_reader :event, :args, :block
6
6
 
7
- def initialize(event,buttons = [],&block)
7
+ alias_method :buttons, :args
8
+
9
+ def initialize(event,args=[],&block)
8
10
  @event = event
9
- @buttons = buttons
11
+ @args = args
10
12
  @block = block
11
13
  end
12
14
 
@@ -1,4 +1,4 @@
1
- require_relative 'event_factory'
1
+ require_relative 'event_dictionary'
2
2
 
3
3
  module Metro
4
4
 
@@ -90,16 +90,15 @@ module Metro
90
90
  # This example uses a block instead of a method name but it is absolultey the same
91
91
  # as the last example.
92
92
  #
93
- def event(event_type,*buttons,&block)
94
- scene_event = EventFactory.new event_type, buttons, &block
95
- events.push scene_event
93
+ def event(event_type,*args,&block)
94
+ EventDictionary.add target: metro_name, type: event_type, args: args, block: block
96
95
  end
97
96
 
98
97
  #
99
- # @return a list of all the EventFactories defined for the scene
98
+ # @return a list of all the EventFactories defined for this event holding object
100
99
  #
101
100
  def events
102
- @events ||= []
101
+ EventDictionary.events_for_targets(hierarchy)
103
102
  end
104
103
 
105
104
  end
@@ -27,7 +27,7 @@ module Metro
27
27
  end
28
28
 
29
29
  def center
30
- [ width / 2 , height / 2 ]
30
+ Point.at width / 2 , height / 2
31
31
  end
32
32
 
33
33
  def fullscreen?
@@ -0,0 +1,21 @@
1
+ module Metro
2
+ class Dimensions < Struct.new(:width,:height)
3
+
4
+ def self.none
5
+ new 0.0, 0.0
6
+ end
7
+
8
+ def self.of(width,height)
9
+ new width.to_f, height.to_f
10
+ end
11
+
12
+ def self.parse(string)
13
+ to *string.split(",",2).map(&:to_f)
14
+ end
15
+
16
+ def to_s
17
+ "#{width},#{height}"
18
+ end
19
+
20
+ end
21
+ end
@@ -1,5 +1,6 @@
1
1
  require_relative 'key_value_coding'
2
2
  require_relative 'rectangle_bounds'
3
+ require_relative 'properties/property'
3
4
 
4
5
  module Metro
5
6
 
@@ -14,6 +15,120 @@ module Metro
14
15
  #
15
16
  class Model
16
17
 
18
+ #
19
+ # This is called every update interval while the actor is in the scene
20
+ #
21
+ # @note This method should be implemented in the Model subclass
22
+ #
23
+ def update ; end
24
+
25
+ #
26
+ # This is called after an update. A model normally is not removed after
27
+ # an update, however if the model responds true to #completed? then it
28
+ # will be removed.
29
+ #
30
+ # @note This method should be implemented in the Model sublclass if you
31
+ # are interested in having the model be removed from the scene.
32
+ #
33
+ def completed? ; false ; end
34
+
35
+
36
+ def self.property(name,options={})
37
+
38
+ # Use the name as the property type if one has not been provided.
39
+
40
+ property_type = options[:type] || name
41
+
42
+ property_class = Property.property(property_type)
43
+
44
+ # When the name does not match the property type then we want to force
45
+ # the prefixing to be on for our sub-properties. This is to make sure
46
+ # that when people define multiple fonts and colors that they do not
47
+ # overlap.
48
+
49
+ override_prefix = !(name == property_type)
50
+
51
+ # Define any properties defined on this property
52
+
53
+ property_class.defined_properties.each do |subproperty|
54
+ sub_options = { prefix: override_prefix }.merge(subproperty.options)
55
+ sub_options = sub_options.merge(parents: (Array(sub_options[:parents]) + [name]))
56
+
57
+ property subproperty.name, sub_options
58
+ end
59
+
60
+ # To ensure that our sub-properties are aware of the of their
61
+ # parent property for which they are dependent on we will need
62
+ # to know this information because we need to behave appropriately
63
+ # within the getter and setter blocks below.
64
+
65
+ is_a_dependency = true if options[:parents]
66
+
67
+ if is_a_dependency
68
+
69
+ parents = Array(options[:parents])
70
+
71
+ method_name = name
72
+
73
+ if options[:prefix]
74
+ method_name = (parents + [name]).join("_")
75
+ end
76
+
77
+ # Define a getter for the sub-property that will traverse the
78
+ # parent properties, finally returning the filtered value
79
+
80
+ define_method method_name do
81
+ raw_value = (parents + [name]).inject(self) {|current,method| current.send(method) }
82
+ property_class.new(self,options).get raw_value
83
+ end
84
+
85
+ # Define a setter for the sub-property that will find the parent
86
+ # value and set itself on that with the filtered value. The parent
87
+ # is then set.
88
+ #
89
+ # @TODO: If getters return dups and not instances of the original object then a very
90
+ # deep setter will not be valid.
91
+ #
92
+ define_method "#{method_name}=" do |value|
93
+ parent_value = parents.inject(self) {|current,method| current.send(method) }
94
+
95
+ prepared_value = property_class.new(self,options).set(value)
96
+ parent_value.send("#{name}=",prepared_value)
97
+
98
+ send("#{parents.last}=",parent_value)
99
+ end
100
+
101
+ else
102
+
103
+ define_method name do
104
+ raw_value = properties[name]
105
+ property_class.new(self,options).get raw_value
106
+ end
107
+
108
+ define_method "#{name}=" do |value|
109
+ prepared_value = property_class.new(self,options).set(value)
110
+ properties[name] = prepared_value
111
+ end
112
+
113
+ end
114
+
115
+ end
116
+
117
+ def properties
118
+ @properties ||= {}
119
+ end
120
+
121
+ property :model, type: :text
122
+ property :name, type: :text
123
+
124
+ #
125
+ # This is called after every {#update} and when the OS wants the window to
126
+ # repaint itself.
127
+ #
128
+ # @note This method should be implemented in the Model subclass.
129
+ #
130
+ def draw ; end
131
+
17
132
  #
18
133
  # The window that this model that this window is currently being
19
134
  # displayed.
@@ -59,46 +174,6 @@ module Metro
59
174
  #
60
175
  include HasEvents
61
176
 
62
- #
63
- # Returns the color of the model. In most cases where color is a prominent
64
- # attribute (e.g. label) this will be the color. In the cases where color
65
- # is less promenint (e.g. image) this will likely be a color that can be
66
- # used to influence the drawing of it.
67
- #
68
- # @see #alpha
69
- #
70
- def color
71
- @color
72
- end
73
-
74
- #
75
- # Sets the color of the model.
76
- #
77
- # @param [String,Fixnum,Gosu::Color] value the new color to set.
78
- #
79
- def color=(value)
80
- @color = Gosu::Color.new(value)
81
- end
82
-
83
- #
84
- # @return the alpha value of the model's color. This is an integer value
85
- # between 0 and 255.
86
- #
87
- def alpha
88
- color.alpha
89
- end
90
-
91
- #
92
- # Sets the alpha of the model.
93
- #
94
- # @param [String,Fixnum] value the new value of the alpha level for the model.
95
- # This value should be between 0 and 255.
96
- #
97
- def alpha=(value)
98
- # TODO: coerce the value is between 0 and 255
99
- color.alpha = value.to_i
100
- end
101
-
102
177
  def saveable?
103
178
  true
104
179
  end
@@ -107,7 +182,7 @@ module Metro
107
182
  def contains?(x,y)
108
183
  false
109
184
  end
110
-
185
+
111
186
  # Belongs to positionable items only
112
187
  def offset(x,y)
113
188
  self.x += x
@@ -121,6 +196,7 @@ module Metro
121
196
  # method or done with care to ensure that functionality is preserved.
122
197
  #
123
198
  def initialize(options = {})
199
+ _load(options)
124
200
  after_initialize
125
201
  end
126
202
 
@@ -135,22 +211,29 @@ module Metro
135
211
  options = {} unless options
136
212
 
137
213
  options.each do |raw_key,value|
138
-
139
214
  key = raw_key.to_s.dup
140
215
  key = key.gsub(/-/,'_').underscore
141
216
 
217
+
142
218
  unless respond_to? key
143
- self.class.send :define_method, key do
144
- instance_variable_get("@#{key}")
219
+ log.warn "Unknown Property #{key}"
220
+ self.class.send(:define_method,key) do
221
+ properties[key]
145
222
  end
223
+ #raise "Do not know (#{key}) property"
146
224
  end
147
225
 
148
226
  unless respond_to? "#{key}="
149
- self.class.send :define_method, "#{key}=" do |value|
150
- instance_variable_set("@#{key}",value)
227
+ log.warn "Unknown Property #{key}="
228
+ self.class.send(:define_method,"#{key}=") do |value|
229
+ properties[key] = value
151
230
  end
231
+
232
+ # raise "Do not know (#{key}=) property"
152
233
  end
153
234
 
235
+
236
+
154
237
  _loaded_options.push key
155
238
  send "#{key}=", value
156
239
  end
@@ -194,6 +277,20 @@ module Metro
194
277
  { name => hash }
195
278
  end
196
279
 
280
+ #
281
+ # @return a common name that can be used through the system as a common identifier.
282
+ #
283
+ def self.metro_name
284
+ name.underscore
285
+ end
286
+
287
+ #
288
+ # @return an array of all ancestor models by name
289
+ #
290
+ def self.hierarchy
291
+ ancestors.find_all {|a| a.respond_to? :metro_name }.map(&:metro_name)
292
+ end
293
+
197
294
  #
198
295
  # Captures all classes that subclass Model.
199
296
  #
@@ -237,9 +334,9 @@ module Metro
237
334
  end
238
335
  end
239
336
 
240
- require_relative 'generic'
241
- require_relative 'label'
242
- require_relative 'menu'
243
- require_relative 'image'
244
- require_relative 'rectangle'
245
- require_relative 'grid_drawer'
337
+ require_relative 'models/generic'
338
+ require_relative 'models/label'
339
+ require_relative 'models/menu'
340
+ require_relative 'models/image'
341
+ require_relative 'models/rectangle'
342
+ require_relative 'models/grid_drawer'