rgame 0.2.0 → 0.3.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.
Files changed (253) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +144 -1
  3. data/README.md +67 -65
  4. data/docs/api/README.md +86 -82
  5. data/docs/api/app.md +125 -68
  6. data/docs/api/assets.md +203 -165
  7. data/docs/api/audio.md +130 -89
  8. data/docs/api/cli.md +259 -0
  9. data/docs/api/components.md +1032 -265
  10. data/docs/api/drawing.md +160 -160
  11. data/docs/api/examples.md +263 -0
  12. data/docs/api/game.md +149 -50
  13. data/docs/api/images.md +39 -39
  14. data/docs/api/input.md +226 -148
  15. data/docs/api/internals.md +241 -62
  16. data/docs/api/localization.md +285 -0
  17. data/docs/api/scene_graph.md +397 -244
  18. data/docs/api/signals.md +81 -76
  19. data/docs/api/systems.md +186 -73
  20. data/docs/api/text.md +58 -41
  21. data/docs/api/tile_maps.md +237 -0
  22. data/docs/api/toolbox.md +395 -146
  23. data/docs/api/ui.md +883 -54
  24. data/docs/api/values.md +216 -33
  25. data/examples/assets/README.md +322 -0
  26. data/examples/assets/blip.ogg +0 -0
  27. data/examples/assets/glyphs.json +5 -0
  28. data/examples/assets/glyphs.png +0 -0
  29. data/examples/assets/hero.json +12 -0
  30. data/examples/assets/hero.png +0 -0
  31. data/examples/assets/icons.json +13 -0
  32. data/examples/assets/icons.png +0 -0
  33. data/examples/assets/music.ogg +0 -0
  34. data/examples/assets/skills.json +10 -0
  35. data/examples/assets/skills.png +0 -0
  36. data/examples/assets/tileset.png +0 -0
  37. data/examples/assets/tileset.tsx +65 -0
  38. data/examples/assets/town.tmx +26 -0
  39. data/examples/assets/ui.json +11 -0
  40. data/examples/assets/ui.png +0 -0
  41. data/examples/collision/locales/en.yml +8 -0
  42. data/examples/collision/main.rb +316 -0
  43. data/examples/collision_tiles/locales/en.yml +9 -0
  44. data/examples/collision_tiles/main.rb +274 -0
  45. data/examples/fullscreen/locales/en.yml +10 -0
  46. data/examples/fullscreen/main.rb +216 -0
  47. data/examples/game_menu/locales/en.yml +8 -0
  48. data/examples/game_menu/main.rb +170 -0
  49. data/examples/input_glyphs/locales/en.yml +14 -0
  50. data/examples/input_glyphs/main.rb +213 -0
  51. data/examples/jump_topdown/locales/en.yml +9 -0
  52. data/examples/jump_topdown/main.rb +178 -0
  53. data/examples/localization/locales/de.yml +12 -0
  54. data/examples/localization/locales/en.yml +13 -0
  55. data/examples/localization/main.rb +158 -0
  56. data/examples/menu_navigation/locales/en.yml +23 -0
  57. data/examples/menu_navigation/main.rb +365 -0
  58. data/examples/music/locales/en.yml +7 -0
  59. data/examples/music/main.rb +134 -0
  60. data/examples/pathfinding/locales/en.yml +17 -0
  61. data/examples/pathfinding/main.rb +298 -0
  62. data/examples/pooling/locales/en.yml +7 -0
  63. data/examples/pooling/main.rb +259 -0
  64. data/examples/quick_wheel/locales/en.yml +16 -0
  65. data/examples/quick_wheel/main.rb +184 -0
  66. data/examples/radial_menu/locales/en.yml +16 -0
  67. data/examples/radial_menu/main.rb +184 -0
  68. data/examples/save_load/locales/en.yml +11 -0
  69. data/examples/save_load/main.rb +207 -0
  70. data/examples/save_load_ids/locales/en.yml +11 -0
  71. data/examples/save_load_ids/main.rb +322 -0
  72. data/examples/scroll_map/locales/en.yml +4 -0
  73. data/examples/scroll_map/main.rb +140 -0
  74. data/examples/signals/locales/en.yml +6 -0
  75. data/examples/signals/main.rb +278 -0
  76. data/examples/skill_bar/locales/en.yml +14 -0
  77. data/examples/skill_bar/main.rb +159 -0
  78. data/examples/sound/locales/en.yml +6 -0
  79. data/examples/sound/main.rb +122 -0
  80. data/examples/split_screen/locales/en.yml +9 -0
  81. data/examples/split_screen/main.rb +304 -0
  82. data/examples/sprite/locales/en.yml +8 -0
  83. data/examples/sprite/main.rb +180 -0
  84. data/examples/timer/locales/en.yml +12 -0
  85. data/examples/timer/main.rb +273 -0
  86. data/examples/velocity/locales/en.yml +6 -0
  87. data/examples/velocity/main.rb +196 -0
  88. data/examples/walk/locales/en.yml +4 -0
  89. data/examples/walk/main.rb +99 -0
  90. data/exe/rgame +9 -0
  91. data/ext/rgame_core/app/app.c +33 -3
  92. data/ext/rgame_core/app/locale.c +67 -0
  93. data/ext/rgame_core/app/locale.h +28 -0
  94. data/ext/rgame_core/audio/audio.c +39 -2
  95. data/ext/rgame_core/example.rb +0 -49
  96. data/ext/rgame_core/extconf.rb +0 -125
  97. data/ext/rgame_core/include/rgame/core.h +38 -2
  98. data/ext/rgame_core/ruby/audio_ext.c +10 -5
  99. data/ext/rgame_core/ruby/core_ext.c +30 -7
  100. data/ext/rgame_core/ruby/core_ext.h +3 -0
  101. data/ext/rgame_core/ruby/locale_ext.c +44 -0
  102. data/ext/rgame_core/ruby/recording_ext.c +1 -1
  103. data/ext/rgame_core/ruby/renderer_ext.c +20 -20
  104. data/ext/rgame_util/extconf.rb +2 -20
  105. data/ext/rgame_util/route_search.c +305 -0
  106. data/ext/rgame_util/route_search.h +86 -0
  107. data/ext/rgame_util/route_search_ext.c +150 -0
  108. data/ext/rgame_util/solid_grid.c +58 -0
  109. data/ext/rgame_util/solid_grid.h +49 -0
  110. data/ext/rgame_util/solid_grid_ext.c +161 -0
  111. data/ext/rgame_util/tile_sweep.c +164 -0
  112. data/ext/rgame_util/tile_sweep.h +62 -0
  113. data/ext/rgame_util/tile_sweep_ext.c +155 -0
  114. data/ext/rgame_util/util_ext.c +3 -0
  115. data/ext/rgame_util/util_ext.h +15 -0
  116. data/lib/rgame/boot.rb +0 -10
  117. data/lib/rgame/cli/new_project.rb +139 -0
  118. data/lib/rgame/cli/templates/Gemfile.tt +23 -0
  119. data/lib/rgame/cli/templates/README.md.tt +93 -0
  120. data/lib/rgame/cli/templates/Rakefile.tt +9 -0
  121. data/lib/rgame/cli/templates/assets/locales/en.yml.tt +10 -0
  122. data/lib/rgame/cli/templates/game.rb.tt +23 -0
  123. data/lib/rgame/cli/templates/gitignore.tt +12 -0
  124. data/lib/rgame/cli/templates/main.rb.tt +11 -0
  125. data/lib/rgame/cli/templates/nodes/root.rb.tt +24 -0
  126. data/lib/rgame/cli/templates/rspec.tt +2 -0
  127. data/lib/rgame/cli/templates/rubocop.yml.tt +75 -0
  128. data/lib/rgame/cli/templates/ruby-version.tt +1 -0
  129. data/lib/rgame/cli/templates/spec/locales_spec.rb.tt +18 -0
  130. data/lib/rgame/cli/templates/spec/nodes/root_spec.rb.tt +18 -0
  131. data/lib/rgame/cli/templates/spec/spec_helper.rb.tt +37 -0
  132. data/lib/rgame/cli.rb +66 -0
  133. data/lib/rgame/core/app.rb +6 -44
  134. data/lib/rgame/core/asset_manager.rb +13 -31
  135. data/lib/rgame/core/audio.rb +37 -16
  136. data/lib/rgame/core/font.rb +0 -3
  137. data/lib/rgame/core/locale.rb +22 -0
  138. data/lib/rgame/core/nine_slice.rb +0 -21
  139. data/lib/rgame/core/renderer.rb +6 -63
  140. data/lib/rgame/core/sprite_sheet.rb +0 -3
  141. data/lib/rgame/core/tile_map_renderer.rb +2 -19
  142. data/lib/rgame/core/ui_atlas.rb +28 -13
  143. data/lib/rgame/core.rb +1 -8
  144. data/lib/rgame/engine/actor_blockers.rb +131 -0
  145. data/lib/rgame/engine/animation_set.rb +1 -0
  146. data/lib/rgame/engine/audio_director.rb +36 -6
  147. data/lib/rgame/engine/bounds_blockers.rb +74 -0
  148. data/lib/rgame/engine/camera.rb +3 -3
  149. data/lib/rgame/engine/circle_collider.rb +4 -2
  150. data/lib/rgame/engine/collision_box.rb +26 -1
  151. data/lib/rgame/engine/collision_system.rb +110 -22
  152. data/lib/rgame/engine/component.rb +34 -10
  153. data/lib/rgame/engine/components/action_trigger.rb +0 -1
  154. data/lib/rgame/engine/components/animated_sprite.rb +27 -25
  155. data/lib/rgame/engine/components/box_collider.rb +99 -0
  156. data/lib/rgame/engine/components/camera_follow.rb +6 -5
  157. data/lib/rgame/engine/components/character_body.rb +20 -61
  158. data/lib/rgame/engine/components/circle_collider.rb +47 -11
  159. data/lib/rgame/engine/components/collision_world.rb +159 -31
  160. data/lib/rgame/engine/components/despawn_offscreen.rb +24 -8
  161. data/lib/rgame/engine/components/feet_collider.rb +61 -0
  162. data/lib/rgame/engine/components/hop.rb +76 -0
  163. data/lib/rgame/engine/components/identity.rb +73 -0
  164. data/lib/rgame/engine/components/mover.rb +285 -0
  165. data/lib/rgame/engine/components/navigator.rb +145 -0
  166. data/lib/rgame/engine/components/path_follow.rb +123 -31
  167. data/lib/rgame/engine/components/player_controller.rb +5 -2
  168. data/lib/rgame/engine/components/pool.rb +1 -1
  169. data/lib/rgame/engine/components/screen_wrap.rb +33 -11
  170. data/lib/rgame/engine/components/sprite.rb +12 -6
  171. data/lib/rgame/engine/components/targeting.rb +9 -11
  172. data/lib/rgame/engine/components/thrust_controller.rb +1 -1
  173. data/lib/rgame/engine/components/tile_world.rb +41 -13
  174. data/lib/rgame/engine/components/timer.rb +1 -1
  175. data/lib/rgame/engine/components/velocity.rb +23 -7
  176. data/lib/rgame/engine/components/wander_controller.rb +6 -2
  177. data/lib/rgame/engine/components/world.rb +133 -0
  178. data/lib/rgame/engine/contact_set.rb +74 -0
  179. data/lib/rgame/engine/culling.rb +4 -6
  180. data/lib/rgame/engine/debug_overlay.rb +5 -11
  181. data/lib/rgame/engine/i18n/plural.rb +45 -0
  182. data/lib/rgame/engine/i18n/plural_rules.rb +82 -0
  183. data/lib/rgame/engine/i18n/template.rb +59 -0
  184. data/lib/rgame/engine/i18n.rb +276 -51
  185. data/lib/rgame/engine/input/action_mapper.rb +0 -26
  186. data/lib/rgame/engine/input/actions.rb +2 -8
  187. data/lib/rgame/engine/input/input_map.rb +39 -21
  188. data/lib/rgame/engine/nav_grid.rb +87 -0
  189. data/lib/rgame/engine/node2d.rb +248 -133
  190. data/lib/rgame/engine/path.rb +4 -6
  191. data/lib/rgame/engine/players.rb +6 -13
  192. data/lib/rgame/engine/presentation.rb +171 -0
  193. data/lib/rgame/engine/scene/scene_stack.rb +4 -4
  194. data/lib/rgame/engine/sealed_privates.rb +54 -0
  195. data/lib/rgame/engine/spatial_hash.rb +51 -19
  196. data/lib/rgame/engine/text.rb +194 -0
  197. data/lib/rgame/engine/tile_blockers.rb +63 -0
  198. data/lib/rgame/engine/tile_map.rb +3 -5
  199. data/lib/rgame/engine/tile_map_layer.rb +0 -2
  200. data/lib/rgame/engine/tileset.rb +2 -4
  201. data/lib/rgame/engine/timer.rb +2 -2
  202. data/lib/rgame/engine/ui/button.rb +248 -0
  203. data/lib/rgame/engine/ui/column.rb +20 -0
  204. data/lib/rgame/engine/ui/icon_button.rb +93 -0
  205. data/lib/rgame/engine/ui/menu.rb +246 -71
  206. data/lib/rgame/engine/ui/navigation.rb +57 -0
  207. data/lib/rgame/engine/ui/nine_slice_style.rb +50 -0
  208. data/lib/rgame/engine/ui/option_button.rb +163 -0
  209. data/lib/rgame/engine/ui/panel_button.rb +32 -0
  210. data/lib/rgame/engine/ui/panel_menu.rb +36 -0
  211. data/lib/rgame/engine/ui/pointing.rb +146 -0
  212. data/lib/rgame/engine/ui/radial_menu.rb +85 -0
  213. data/lib/rgame/engine/ui/ring.rb +55 -0
  214. data/lib/rgame/engine/ui/row.rb +21 -0
  215. data/lib/rgame/engine/ui/shape_style.rb +102 -0
  216. data/lib/rgame/engine/ui/stack.rb +58 -0
  217. data/lib/rgame/engine/ui/stepping.rb +93 -0
  218. data/lib/rgame/engine/ui/text_button.rb +59 -0
  219. data/lib/rgame/engine/viewports.rb +2 -5
  220. data/lib/rgame/engine/world_view.rb +5 -4
  221. data/lib/rgame/engine.rb +31 -25
  222. data/lib/rgame/game.rb +99 -27
  223. data/lib/rgame/rubocop/cop/game/draw_in_local_space.rb +103 -0
  224. data/lib/rgame/rubocop/cop/game/hot_path.rb +36 -0
  225. data/lib/rgame/rubocop/cop/game/layer_boundary.rb +43 -0
  226. data/lib/rgame/rubocop/cop/game/no_core_in_engine_layer.rb +100 -0
  227. data/lib/rgame/rubocop/cop/game/no_engine_in_core_layer.rb +84 -0
  228. data/lib/rgame/rubocop/cop/game/no_interpolation_in_hot_path.rb +50 -0
  229. data/lib/rgame/rubocop/cop/game/no_literal_text.rb +41 -0
  230. data/lib/rgame/rubocop/cop/game/no_needless_allocation.rb +112 -0
  231. data/lib/rgame/rubocop/default.yml +39 -0
  232. data/lib/rgame/rubocop/plugin.rb +45 -0
  233. data/lib/rgame/rubocop.rb +11 -0
  234. data/lib/rgame/util/color.rb +20 -24
  235. data/lib/rgame/util/controls.rb +14 -28
  236. data/lib/rgame/util/route_search.rb +27 -0
  237. data/lib/rgame/util/save_file.rb +107 -0
  238. data/lib/rgame/util/solid_grid.rb +37 -0
  239. data/lib/rgame/util/tensor.rb +0 -9
  240. data/lib/rgame/util/tile_sweep.rb +36 -0
  241. data/lib/rgame/util/z.rb +4 -14
  242. data/lib/rgame/util.rb +4 -3
  243. data/lib/rgame/version.rb +1 -1
  244. data/lib/rgame.rb +0 -15
  245. metadata +167 -12
  246. data/lib/rgame/engine/actor.rb +0 -53
  247. data/lib/rgame/engine/body.rb +0 -49
  248. data/lib/rgame/engine/cached_label.rb +0 -33
  249. data/lib/rgame/engine/input/player_controller.rb +0 -14
  250. data/lib/rgame/engine/matrix.rb +0 -32
  251. data/lib/rgame/engine/resettable.rb +0 -67
  252. data/lib/rgame/engine/tile_collision.rb +0 -78
  253. data/lib/rgame/engine/ui/menu_item.rb +0 -84
@@ -12,7 +12,7 @@ module RGame
12
12
  # app.assets.release(:level1)
13
13
  #
14
14
  # The built-in types are `image`, `sound`, `song` and `read`, plus the
15
- # composites `sheet` and `ui_atlas`. A game or its glue adds more with
15
+ # composites `sheet` and `ui_atlas`; `glob` lists what is there to load. A game or its glue adds more with
16
16
  # {#add_loader}, which is how a tile map gets loaded without Core having to
17
17
  # know what one is.
18
18
  #
@@ -55,7 +55,6 @@ module RGame
55
55
  # `loaders:` is what lets the caching, path resolution and grouping be
56
56
  # specced with no window, no GL context and no files at all.
57
57
  class AssetManager
58
- # Owner of every ungrouped load. Never released, only cleared.
59
58
  PERMANENT = :__permanent__
60
59
 
61
60
  # `app` is what images are loaded into and where the audio device comes
@@ -65,7 +64,7 @@ module RGame
65
64
  @app = app
66
65
  @loaders = {}
67
66
  @cache = {}
68
- @owners = {} # cache key => Set of groups holding it
67
+ @owners = {}
69
68
 
70
69
  (loaders || default_loaders).each { |type, loader| add_loader(type, &loader) }
71
70
  end
@@ -91,9 +90,6 @@ module RGame
91
90
  # offering it an id.
92
91
  def add_loader(type, &loader)
93
92
  @loaders[type] = loader
94
- # A singleton method rather than a class-level one: two managers may
95
- # know different types, and a game that adds `:tilemap` should not be
96
- # teaching it to everyone else's.
97
93
  define_singleton_method(type) { |path, group = PERMANENT| leaf(type, path, group) }
98
94
  self
99
95
  end
@@ -130,10 +126,6 @@ module RGame
130
126
  # Takes `group` off every asset's owner set and drops whatever no group
131
127
  # still holds.
132
128
  def release(group)
133
- # Without this, `release(PERMANENT)` would empty every ungrouped
134
- # asset's owner set and drop the lot — the exact opposite of what the
135
- # constant's name promises, and silent. It is only reachable by naming
136
- # the sentinel, so saying what to use instead beats ignoring the call.
137
129
  raise ArgumentError, 'ungrouped assets are dropped by #clear, not #release' if group == PERMANENT
138
130
 
139
131
  @owners.each_value { |groups| groups.delete(group) }
@@ -153,15 +145,23 @@ module RGame
153
145
  self
154
146
  end
155
147
 
148
+ # The paths under the media root that match `pattern`, relative to the
149
+ # root and sorted, so what a caller loads from the list — and the order it
150
+ # merges in — is the same on every platform and file system:
151
+ #
152
+ # assets.glob('locales/**/*.yml') # => ["locales/de.yml", "locales/en.yml"]
153
+ #
154
+ # A directory that does not exist matches nothing, so a game without one
155
+ # gets `[]` rather than an error. An absolute pattern is used as it
156
+ # stands, and its matches come back absolute. Nothing is loaded or cached.
157
+ def glob(pattern) = Dir.glob(pattern, base: File.expand_path(@root)).sort
158
+
156
159
  # How many assets are cached. For tests and for a debug overlay; a game
157
160
  # has no reason to ask.
158
161
  def size = @cache.size
159
162
 
160
163
  private
161
164
 
162
- # The app is reached through the ivar rather than captured, so
163
- # `app.audio` is not called until a sound is actually asked for — loading
164
- # an image opens no sound device.
165
165
  def default_loaders
166
166
  {
167
167
  image: ->(path) { Image.new(@app, path) },
@@ -175,20 +175,8 @@ module RGame
175
175
  fetch(type, path, group) { @loaders.fetch(type).call(resolve(path)) }
176
176
  end
177
177
 
178
- # Where a path actually is on disk.
179
- #
180
- # `expand_path` rather than `join` for two reasons. An **absolute** path
181
- # is used as it stands, which is what lets a loader hand one back — the
182
- # tile-map loader gets its tileset image that way, since the path comes
183
- # out of a `.tsx` that was itself found on disk. And `'a/./b.png'`,
184
- # `'a/../b.png'` and `'b.png'` all land on one cache key rather than
185
- # three, so a file cannot be loaded twice by being named twice.
186
178
  def resolve(path) = File.expand_path(path, @root)
187
179
 
188
- # Memoise on miss, then record the owning group — so a cache *hit* under a
189
- # new group is tagged too. Tagging after the load rather than before is
190
- # what leaves a failed load with no owner behind it, which makes a retry a
191
- # clean retry rather than a permanently half-registered asset.
192
180
  def fetch(type, path, group)
193
181
  key = [type, resolve(path)]
194
182
  object = (@cache[key] ||= yield)
@@ -204,17 +192,11 @@ module RGame
204
192
  UiAtlas.new(*composite_parts(relative_path, group))
205
193
  end
206
194
 
207
- # The two halves a composite is made of, both through the cache: the
208
- # descriptor as a cached `read`, and the image it names as a cached
209
- # `image` resolved beside it.
210
195
  def composite_parts(relative_path, group)
211
196
  data = JSON.parse(read(relative_path, group), symbolize_names: true)
212
197
  [image(sibling(relative_path, data[:image]), group), data]
213
198
  end
214
199
 
215
- # A path next to `relative_path`, still relative to the root — so a
216
- # descriptor's image lands on the same `[:image, path]` cache key a
217
- # standalone `image` of that file would.
218
200
  def sibling(relative_path, name)
219
201
  directory = File.dirname(relative_path)
220
202
  directory == '.' ? name : File.join(directory, name)
@@ -19,6 +19,21 @@ module RGame
19
19
  # the alternative is a crash at startup for something nobody asked for.
20
20
  # `#backend` says which one was chosen.
21
21
  class Audio
22
+ # `assets` is what a path id is resolved through, and may be nil — a
23
+ # device built by hand plays only what it is handed. `RGame::Core::App`
24
+ # passes its own manager, so a game never sets this.
25
+ #
26
+ # A Ruby `self.new` because the C `initialize` takes no arguments and has
27
+ # no business knowing what an asset manager is — the same shape
28
+ # `Renderer.new` uses for the same reason.
29
+ def self.new(assets: nil)
30
+ audio = super()
31
+ audio.assets = assets
32
+ audio
33
+ end
34
+
35
+ attr_accessor :assets
36
+
22
37
  # Loads a short sound to play over itself. The same thing as
23
38
  # `Sample.new(audio, path)`, and the form to prefer: it reads in the
24
39
  # direction the objects depend, and it is the form a stand-in device can
@@ -28,15 +43,6 @@ module RGame
28
43
  # Loads a long one to stream. See {Song}.
29
44
  def song(path) = Song.new(self, path)
30
45
 
31
- # --- play-by-id -----------------------------------------------------
32
- #
33
- # The same boundary the renderer's draw-by-id serves: game logic emits
34
- # facts — "the ship was hit" — and names the sound, because a scene may
35
- # not hold a `Sample`. Registration only, unlike the renderer: a sound id
36
- # is whatever a game wants to call it, and there is no per-frame path to
37
- # make resolving one worth caching.
38
- #
39
- # audio.register_sound(:hit, app.assets.sound('example 09/hurt.ogg'))
40
46
  # audio.play_sound(:hit)
41
47
 
42
48
  def register_sound(id, sample)
@@ -49,17 +55,17 @@ module RGame
49
55
  self
50
56
  end
51
57
 
52
- # Plays a registered sample. Each call is another voice, layered over the
53
- # ones already sounding.
58
+ # Plays a sample by id. Each call is another voice, layered over the ones
59
+ # already sounding.
54
60
  def play_sound(id)
55
- samples.fetch(id).play
61
+ lookup(:sound, id).play
56
62
  end
57
63
 
58
- # Starts a registered song looping, and does **nothing** if it is already
59
- # playing — so a scene that re-emits the same request every time it is
60
- # entered never restarts the music mid-loop.
64
+ # Starts a song looping, and does **nothing** if it is already playing —
65
+ # so a scene that re-emits the same request every time it is entered never
66
+ # restarts the music mid-loop.
61
67
  def play_music(id)
62
- song = songs.fetch(id)
68
+ song = lookup(:song, id)
63
69
  return song if song.playing?
64
70
 
65
71
  @playing_song = song
@@ -81,6 +87,21 @@ module RGame
81
87
 
82
88
  def samples = @samples ||= {}
83
89
  def songs = @songs ||= {}
90
+
91
+ def registry(type) = type == :sound ? samples : songs
92
+
93
+ def lookup(type, id)
94
+ raise TypeError, "no implicit conversion of nil into #{type}" if id.nil?
95
+
96
+ table = registry(type)
97
+ table.fetch(id) { table[id] = resolve_asset(type, id) }
98
+ end
99
+
100
+ def resolve_asset(type, id)
101
+ resolved = @assets.public_send(type, id) if id.is_a?(String) && @assets.respond_to?(type)
102
+ resolved || raise(KeyError, "no #{type} registered for #{id.inspect} " \
103
+ 'and no AssetManager to resolve it')
104
+ end
84
105
  end
85
106
 
86
107
  # A short sound, decoded once and played many times over.
@@ -34,9 +34,6 @@ module RGame
34
34
  # Scripts outside the shipped font's coverage — CJK, Arabic, Hebrew — need
35
35
  # their own file. No font of this size covers them.
36
36
  class Font
37
- # Shipped with the gem, alongside its SIL OFL 1.1 licence. Resolved from
38
- # this file's location so it works the same from a checkout and from an
39
- # installed gem.
40
37
  DEFAULT_PATH = File.expand_path('../fonts/LiberationSans-Regular.ttf', __dir__)
41
38
 
42
39
  # `path:` is a keyword for callers but positional for the C initialize,
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rgame/core_ext'
4
+
5
+ module RGame
6
+ # `RGame::Core.preferred_locales` is C (`ext/rgame_core/app/locale.c`, bound in
7
+ # `ruby/locale_ext.c`): the user's preferred locales as the operating system
8
+ # reports them through SDL, most preferred first.
9
+ #
10
+ # RGame::Core.preferred_locales # => ["de-AT", "en"]
11
+ #
12
+ # Each is a language, then a hyphen and a country when the OS names one. The
13
+ # list is `[]` when the OS names none — on Linux, under `LANG=C`. It needs no
14
+ # app: SDL reads it without being initialised, so it is a module function
15
+ # rather than an `App` method.
16
+ #
17
+ # What it returns is taken as given. SDL on Linux reads `LANG` and then
18
+ # `LANGUAGE`, and ignores `LC_ALL`, so the list can repeat a locale; choosing
19
+ # among them is `RGame::Engine::I18n.choose`, which the glue calls.
20
+ module Core
21
+ end
22
+ end
@@ -41,10 +41,6 @@ module RGame
41
41
  # (x, y, w, h) is the source rectangle inside `image`, so one sheet can
42
42
  # hold many of these — which is what UiAtlas does with it.
43
43
  def initialize(image, x:, y:, w:, h:, border:, scale: 1)
44
- # A zero or negative scale would give every tile a step of zero pixels,
45
- # and `#draw`'s tiling loops would never advance. Refusing it here costs
46
- # nothing; a guard inside the loop would cost a branch per tile, per
47
- # widget, per frame.
48
44
  raise ArgumentError, "nine-slice scale must be positive, got #{scale}" unless scale.positive?
49
45
 
50
46
  @scale = scale
@@ -69,10 +65,6 @@ module RGame
69
65
  r = @r * s
70
66
  t = @t * s
71
67
  b = @b * s
72
- # These go negative for a rectangle narrower than its own borders, and
73
- # are not clamped: #band refuses a non-positive size anyway, so clamping
74
- # here would be a second guard that cannot change any outcome — and
75
- # would read as if the case were handled in two places.
76
68
  inner_w = dw - l - r
77
69
  inner_h = dh - t - b
78
70
 
@@ -82,8 +74,6 @@ module RGame
82
74
  band(renderer, @left, dx, dy + t, l, inner_h, z, color)
83
75
  band(renderer, @right, dx + dw - r, dy + t, r, inner_h, z, color)
84
76
 
85
- # Last, so a tile that reached the edge of its band is covered rather
86
- # than showing through a corner's transparent pixels.
87
77
  piece(renderer, @tl, dx, dy, z, color)
88
78
  piece(renderer, @tr, dx + dw - r, dy, z, color)
89
79
  piece(renderer, @bl, dx, dy + dh - b, z, color)
@@ -92,10 +82,6 @@ module RGame
92
82
 
93
83
  private
94
84
 
95
- # A uniform integer becomes four equal sides. Accepting both shapes here
96
- # rather than in the callers is what makes `NineSlice.new(image, border: 7,
97
- # ...)` work on its own — a bare integer would otherwise fail as
98
- # `7[:left]`, which names nothing that appears in the caller's code.
99
85
  def normalize_border(border)
100
86
  return border.transform_keys(&:to_sym) unless border.is_a?(Integer)
101
87
 
@@ -122,11 +108,6 @@ module RGame
122
108
  @centre = cut(image, x + @l, y + @t, centre_w, centre_h)
123
109
  end
124
110
 
125
- # A piece with no pixels is a legitimate part of a legitimate slice — a
126
- # border with no centre column is a bar that only stretches vertically,
127
- # and a zero border is one that does not stretch at all on that side. Such
128
- # a piece is `nil` and simply never drawn; asking `Image#subimage` for it
129
- # would raise.
130
111
  def cut(image, x, y, width, height)
131
112
  return nil unless width.positive? && height.positive?
132
113
 
@@ -139,8 +120,6 @@ module RGame
139
120
  renderer.image_at(image, x, y, scale_x: @scale, scale_y: @scale, z: z, color: color)
140
121
  end
141
122
 
142
- # Repeats `image` across (bx, by, bw, bh), clipped to it so the trailing
143
- # tile is cropped instead of overrunning into the next band.
144
123
  def band(renderer, image, bx, by, bw, bh, z, color)
145
124
  return if image.nil? || bw <= 0 || bh <= 0
146
125
 
@@ -44,8 +44,9 @@ module RGame
44
44
  # what a spec or a bare script gets.
45
45
  #
46
46
  # The C half of this class (ext/rgame_core/ruby/renderer_ext.c) has the `draw_*`
47
- # and `push_*` primitives; everything here is the comfortable surface over
48
- # them.
47
+ # and `push_*` primitives, registered private; everything here is the
48
+ # surface over them, so a draw cannot skip the z offset or the colour
49
+ # coercion, and a push cannot go without its pop.
49
50
  #
50
51
  # ## Colours
51
52
  #
@@ -58,22 +59,14 @@ module RGame
58
59
 
59
60
  Z = RGame::Util::Z
60
61
 
61
- # Shapes default above sprites, so a debug box or a health bar drawn
62
- # without a `z:` lands on top of *that node's* sprite rather than under
63
- # it. Well inside one slot, so the defaults order a node's own drawing and
64
- # nothing further.
65
62
  SHAPE_Z = 50
66
63
  IMAGE_Z = 0
67
64
 
68
- # Enough segments that a circle reads as round at the sizes a 2D game
69
- # draws one, and few enough that a screenful of them is still one batch.
70
65
  CIRCLE_SEGMENTS = 64
71
66
 
72
- # Text defaults above sprites but below shapes, within the same slot.
73
67
  TEXT_Z = 10
74
68
  FONT_SIZE = 18
75
69
 
76
- # Translucent red, for #debug_box.
77
70
  DEBUG_BOX_COLOR = Color.new(255, 40, 40, 120)
78
71
 
79
72
  # `assets:` is where a draw id that is not registered gets resolved from,
@@ -91,32 +84,16 @@ module RGame
91
84
 
92
85
  attr_accessor :assets
93
86
 
94
- # --- draw-by-id ---------------------------------------------------------
95
- #
96
- # Game logic names assets, it does not hold them: the engine layer may
97
- # hold `RGame::Util` values but no `RGame::Core` handle at all, so a
98
- # Symbol or a path is the only thing a node *can* carry. Resolving it is
99
- # this side of the boundary's job.
100
- #
101
- # An id is normally a **root-relative path**, resolved through the asset
102
- # manager and then remembered, so a per-frame draw neither re-resolves nor
103
- # allocates a lookup key:
104
- #
105
- # renderer.sprite('example 09/player.json', row, col, x, y)
106
- #
107
- # `register_*` pre-binds an id to a chosen object, for the two things a
108
- # path cannot name: an id that is not a file (nine-slice ids are *atlas
109
- # element* names) and an object the game assembled itself.
110
-
111
87
  def register_image(id, image) = registry(:image)[id] = image
112
88
  def register_sheet(id, sheet) = registry(:sheet)[id] = sheet
113
89
  def register_tilemap(id, tilemap) = registry(:tilemap)[id] = tilemap
114
90
  def register_nine_slice(id, nine_slice) = registry(:nine_slice)[id] = nine_slice
115
91
 
116
- # Registers every element of a UiAtlas under its own name, since those
117
- # names are what a widget asks for.
92
+ # Registers every element of a UiAtlas its nine-slices and its images —
93
+ # under its own name, since those names are what a widget asks for.
118
94
  def register_ui_atlas(atlas)
119
95
  atlas.nine_slices.each { |id, nine_slice| register_nine_slice(id, nine_slice) }
96
+ atlas.images.each { |id, image| register_image(id, image) }
120
97
  self
121
98
  end
122
99
 
@@ -226,9 +203,6 @@ module RGame
226
203
  begin
227
204
  yield
228
205
  ensure
229
- # An ensure, not a plain pop: a scene that raises mid-draw would
230
- # otherwise leave the stack deeper than it found it, and every
231
- # later frame would draw askew for a reason nothing points at.
232
206
  pop
233
207
  end
234
208
  end
@@ -280,9 +254,6 @@ module RGame
280
254
  begin
281
255
  yield
282
256
  ensure
283
- # An ensure for the same reason `rotated` has one: a scene that raises
284
- # mid-draw would otherwise leave every later node drawing in its
285
- # layer, and the frame would come out interleaved with no clue why.
286
257
  pop
287
258
  end
288
259
  end
@@ -347,9 +318,6 @@ module RGame
347
318
  yield
348
319
  completed = true
349
320
  ensure
350
- # A block that raised leaves a half-built recording open, and the
351
- # next frame would keep drawing into it. Unwinding here means the
352
- # exception is the only thing the caller has to deal with.
353
321
  cancel_record unless completed
354
322
  end
355
323
  end_record
@@ -363,44 +331,19 @@ module RGame
363
331
 
364
332
  private
365
333
 
366
- # Colours cross into C as a packed integer. `Color.coerce` is the single
367
- # place nil/array/Color are turned into one, so no drawing method has its
368
- # own idea of what a colour is.
369
334
  def packed(color) = Color.coerce(color).packed
370
335
 
371
- # An `Image` is already what it is; anything else is an id for one.
372
- #
373
- # The two callers this serves cannot be reconciled any other way. Core's
374
- # own drawing classes — SpriteSheet, NineSlice — hold real images and pass
375
- # them; the engine layer is forbidden from holding one and can only pass
376
- # an id. Dispatching on the type keeps both spellings of `#image` and
377
- # `#background` working without two parallel method names for the same
378
- # picture.
379
336
  def resolve_image(image) = image.is_a?(Image) ? image : lookup(:image, image)
380
337
 
381
338
  def registry(type) = (@registries ||= {})[type] ||= {}
382
339
 
383
- # Prefer an explicit registration, otherwise ask the asset manager, then
384
- # remember the answer — so a per-frame draw neither re-resolves nor
385
- # allocates a lookup key.
386
340
  def lookup(type, id)
387
- # `nil` is never an id, and reporting it as one ("no image registered
388
- # for nil") describes a typo when the actual bug is an asset that
389
- # resolved to nothing. The two want different fixes.
390
341
  raise TypeError, "no implicit conversion of nil into #{type}" if id.nil?
391
342
 
392
343
  table = registry(type)
393
344
  table.fetch(id) { table[id] = resolve_asset(type, id) }
394
345
  end
395
346
 
396
- # Only a String is offered to the asset manager, because only a String can
397
- # be a path. A Symbol id is a name a game chose, so a missing one is the
398
- # KeyError below — naming the id — rather than whatever the manager makes
399
- # of being handed a Symbol where it wanted a filename.
400
- #
401
- # `respond_to?` for the same reason one step along: the manager grows an
402
- # accessor per asset type, and asking for one it does not have yet should
403
- # be this error rather than a NoMethodError from inside it.
404
347
  def resolve_asset(type, id)
405
348
  resolved = @assets.public_send(type, id) if id.is_a?(String) && @assets.respond_to?(type)
406
349
  resolved || raise(KeyError, "no #{type} registered for #{id.inspect} " \
@@ -97,9 +97,6 @@ module RGame
97
97
  end
98
98
  end
99
99
 
100
- # A typo'd or missing key would otherwise surface as a NoMethodError on nil
101
- # from inside the slicing arithmetic, which says nothing about the file
102
- # that is actually wrong.
103
100
  def missing(key)
104
101
  raise ArgumentError, "sprite sheet descriptor has no #{key}"
105
102
  end
@@ -81,8 +81,6 @@ module RGame
81
81
  @tileset = map.tileset
82
82
  @tiles = tiles
83
83
  @animated = collect_animated_tiles
84
- # Baked on first draw, not here: recording needs a live frame, and there
85
- # is no renderer at construction.
86
84
  @static = Array.new(map.layer_count)
87
85
  end
88
86
 
@@ -102,6 +100,8 @@ module RGame
102
100
  # No `z:`. A layer is drawn by a node of its own, so where it sits is the
103
101
  # scene tree's answer; everything this issues belongs to that one node and
104
102
  # goes in its slot.
103
+ #
104
+ # @api private
105
105
  def draw_layer(renderer, index, cull_x, cull_y, cull_width, cull_height, elapsed: 0.0)
106
106
  unless index.is_a?(Integer) && index >= 0 && index < @static.size
107
107
  raise ArgumentError, "no layer #{index.inspect} in this map (it has #{@static.size})"
@@ -115,10 +115,6 @@ module RGame
115
115
 
116
116
  private
117
117
 
118
- # [col, row, local_id] for every animated tile, per layer. Walked once at
119
- # construction: a map is thousands of tiles and a handful of animated
120
- # ones, and finding them again each frame would be the whole cost this
121
- # class exists to avoid.
122
118
  def collect_animated_tiles
123
119
  found = Array.new(@map.layer_count) { [] }
124
120
  each_tile do |layer, col, row, local|
@@ -129,7 +125,6 @@ module RGame
129
125
  found
130
126
  end
131
127
 
132
- # Every non-empty tile of every layer, as [layer, col, row, local_id].
133
128
  def each_tile
134
129
  @map.layer_count.times do |layer|
135
130
  @map.height.times do |row|
@@ -143,9 +138,6 @@ module RGame
143
138
  end
144
139
  end
145
140
 
146
- # Bakes one layer's static tiles into a recording. An empty layer bakes an
147
- # empty recording, which replays as nothing — so a map with a spacer layer
148
- # in it needs no special case here or at the call site.
149
141
  def bake(renderer, index)
150
142
  renderer.record do
151
143
  each_tile do |layer, col, row, local|
@@ -161,15 +153,8 @@ module RGame
161
153
  tile_width = @map.tile_width
162
154
  tile_height = @map.tile_height
163
155
 
164
- # Tiled writes frame durations in milliseconds; everything here counts
165
- # in seconds. Converted once per draw rather than once per tile.
166
156
  ms = (elapsed * 1000.0).to_i
167
157
 
168
- # fdiv, not `/`. With two Integers — an integer camera position, which
169
- # is entirely ordinary — `/` floors first and the `.ceil` below becomes
170
- # a no-op, leaving the last column of tiles undrawn: a one-tile strip of
171
- # nothing along the right and bottom edges of the screen, and only when
172
- # the camera happens to be on a whole pixel.
173
158
  col_start = cull_x.fdiv(tile_width).floor
174
159
  row_start = cull_y.fdiv(tile_height).floor
175
160
  col_end = (cull_x + cull_width).fdiv(tile_width).ceil
@@ -178,8 +163,6 @@ module RGame
178
163
  tiles.each do |col, row, local|
179
164
  next if col < col_start || col >= col_end || row < row_start || row >= row_end
180
165
 
181
- # World coordinates, like the baked band above it: the caller's
182
- # transform is what puts either on screen.
183
166
  renderer.image_at(@tiles[@tileset.frame_local_id(local, ms)],
184
167
  col * tile_width, row * tile_height)
185
168
  end
@@ -7,11 +7,12 @@ require_relative 'nine_slice'
7
7
 
8
8
  module RGame
9
9
  module Core
10
- # One sheet of UI chrome, cut into named nine-slices.
10
+ # One sheet of UI chrome, cut into named nine-slices and named images.
11
11
  #
12
12
  # atlas = RGame::Core::UiAtlas.load(app, 'media/ui.json')
13
13
  # renderer.register_ui_atlas(atlas)
14
14
  # renderer.nine_slice(:button_idle, x, y, width, height)
15
+ # renderer.image(:home, cx, cy)
15
16
  #
16
17
  # A button has four states, a panel has one, a scrollbar has three pieces —
17
18
  # all of them small, and all of them cheaper as sub-rectangles of a single
@@ -29,27 +30,38 @@ module RGame
29
30
  # "panel": { "x": 0, "y": 0, "w": 32, "h": 32,
30
31
  # "border": { "left": 4, "right": 4, "top": 8, "bottom": 4 },
31
32
  # "scale": 2 }
33
+ # },
34
+ # "images": {
35
+ # "home": { "x": 0, "y": 96, "w": 50, "h": 50 },
36
+ # "gear": { "x": 50, "y": 96, "w": 50, "h": 50 }
32
37
  # }
33
38
  # }
34
39
  #
35
- # `image` is resolved next to the descriptor. Each entry is a source
36
- # rectangle plus a border — a uniform integer or one value per side — and an
37
- # optional `scale` overriding the sheet default. See
40
+ # `image` is resolved next to the descriptor. Each nine-slice entry is a
41
+ # source rectangle plus a border — a uniform integer or one value per side —
42
+ # and an optional `scale` overriding the sheet default. See
38
43
  # RGame::Core::NineSlice for what those mean when it is drawn.
39
44
  #
45
+ # Each `images` entry is a rectangle and nothing else: an icon is drawn
46
+ # whole, so it has no border, and no scale either, because how large to draw
47
+ # it is the draw call's `scale:`. Either section may be absent or `null`.
48
+ #
40
49
  # ## Element names, not filenames
41
50
  #
42
- # `nine_slices` is keyed by whatever the descriptor calls each element, and
51
+ # Both sections are keyed by whatever the descriptor calls each element, and
43
52
  # those names are what a widget asks for. That is why nine-slices are the
44
53
  # one asset the renderer resolves by registration only: `:button_focus` is
45
54
  # not a file and never can be. `Renderer#register_ui_atlas` registers every
46
- # element in one call.
55
+ # element of both kinds in one call.
47
56
  #
48
57
  # Parsing happens once, at load. Nothing here is touched again per frame.
49
58
  class UiAtlas
50
- # The elements, by name. Values are `NineSlice`s.
59
+ # The nine-slice elements, by name. Values are `NineSlice`s.
51
60
  attr_reader :nine_slices
52
61
 
62
+ # The image elements, by name. Values are `Image`s cut from the sheet.
63
+ attr_reader :images
64
+
53
65
  # Loads a descriptor and the sheet beside it.
54
66
  #
55
67
  # The `AssetManager` uses `.new` instead, with an image it has already
@@ -65,19 +77,22 @@ module RGame
65
77
  sheet_scale = data[:scale] || 1
66
78
 
67
79
  @nine_slices = (data[:nine_slices] || {}).to_h do |id, spec|
68
- [id, build(image, id, spec, sheet_scale)]
80
+ [id, element(id) { build_nine_slice(image, spec, sheet_scale) }]
81
+ end
82
+ @images = (data[:images] || {}).to_h do |id, spec|
83
+ [id, element(id) { image.subimage(spec[:x], spec[:y], spec[:w], spec[:h]) }]
69
84
  end
70
85
  end
71
86
 
72
87
  private
73
88
 
74
- # A descriptor holds many elements, and a mistake in one of them surfaces
75
- # from inside NineSlice's arithmetic — "undefined method '-' for nil" says
76
- # nothing about *which* button is wrong. Naming the element is the whole
77
- # value this wrapper adds, so it catches broadly on purpose.
78
- def build(image, id, spec, sheet_scale)
89
+ def build_nine_slice(image, spec, sheet_scale)
79
90
  NineSlice.new(image, x: spec[:x], y: spec[:y], w: spec[:w], h: spec[:h],
80
91
  border: spec[:border], scale: spec[:scale] || sheet_scale)
92
+ end
93
+
94
+ def element(id)
95
+ yield
81
96
  rescue StandardError => e
82
97
  raise ArgumentError, "ui atlas element #{id.inspect}: #{e.message}"
83
98
  end
data/lib/rgame/core.rb CHANGED
@@ -1,15 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # RGame::Core — everything that depends on SDL, OpenGL, or anything built on
4
- # them. Requiring this file loads those libraries into the process, which is why
5
- # it is NOT required from lib/rgame.rb; ask for it explicitly:
6
- #
7
- # require "rgame/core"
8
- #
9
- # Its counterpart is RGame::Util (see lib/rgame/util.rb), which stays free of
10
- # graphics dependencies.
11
3
  require_relative 'version'
12
4
  require_relative 'core/app'
5
+ require_relative 'core/locale'
13
6
  require_relative 'core/input'
14
7
  require_relative 'core/gamepad'
15
8
  require_relative 'core/image'