rgame 0.1.0 → 0.3.0

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 (265) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +228 -0
  3. data/README.md +142 -243
  4. data/docs/api/README.md +141 -90
  5. data/docs/api/app.md +125 -68
  6. data/docs/api/assets.md +203 -166
  7. data/docs/api/audio.md +130 -89
  8. data/docs/api/cli.md +259 -0
  9. data/docs/api/components.md +1045 -254
  10. data/docs/api/drawing.md +187 -119
  11. data/docs/api/examples.md +263 -0
  12. data/docs/api/game.md +165 -45
  13. data/docs/api/images.md +39 -39
  14. data/docs/api/input.md +344 -85
  15. data/docs/api/internals.md +241 -62
  16. data/docs/api/localization.md +285 -0
  17. data/docs/api/scene_graph.md +503 -123
  18. data/docs/api/signals.md +81 -76
  19. data/docs/api/systems.md +197 -64
  20. data/docs/api/text.md +58 -41
  21. data/docs/api/tile_maps.md +237 -0
  22. data/docs/api/toolbox.md +400 -147
  23. data/docs/api/ui.md +927 -0
  24. data/docs/api/values.md +234 -19
  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/README.md +6 -5
  92. data/ext/rgame_core/app/app.c +215 -11
  93. data/ext/rgame_core/app/locale.c +67 -0
  94. data/ext/rgame_core/app/locale.h +28 -0
  95. data/ext/rgame_core/audio/audio.c +113 -2
  96. data/ext/rgame_core/example.rb +18 -56
  97. data/ext/rgame_core/extconf.rb +16 -113
  98. data/ext/rgame_core/graphics/canvas.c +45 -4
  99. data/ext/rgame_core/graphics/canvas.h +65 -10
  100. data/ext/rgame_core/graphics/clip.c +22 -13
  101. data/ext/rgame_core/include/rgame/core.h +151 -5
  102. data/ext/rgame_core/input/gamepad.c +57 -3
  103. data/ext/rgame_core/ruby/audio_ext.c +10 -5
  104. data/ext/rgame_core/ruby/core_ext.c +46 -7
  105. data/ext/rgame_core/ruby/core_ext.h +3 -0
  106. data/ext/rgame_core/ruby/locale_ext.c +44 -0
  107. data/ext/rgame_core/ruby/recording_ext.c +1 -1
  108. data/ext/rgame_core/ruby/renderer_ext.c +42 -19
  109. data/ext/rgame_util/color_ext.c +12 -3
  110. data/ext/rgame_util/extconf.rb +2 -20
  111. data/ext/rgame_util/route_search.c +305 -0
  112. data/ext/rgame_util/route_search.h +86 -0
  113. data/ext/rgame_util/route_search_ext.c +150 -0
  114. data/ext/rgame_util/solid_grid.c +58 -0
  115. data/ext/rgame_util/solid_grid.h +49 -0
  116. data/ext/rgame_util/solid_grid_ext.c +161 -0
  117. data/ext/rgame_util/tile_sweep.c +164 -0
  118. data/ext/rgame_util/tile_sweep.h +62 -0
  119. data/ext/rgame_util/tile_sweep_ext.c +155 -0
  120. data/ext/rgame_util/util_ext.c +3 -0
  121. data/ext/rgame_util/util_ext.h +15 -0
  122. data/lib/rgame/boot.rb +0 -10
  123. data/lib/rgame/cli/new_project.rb +139 -0
  124. data/lib/rgame/cli/templates/Gemfile.tt +23 -0
  125. data/lib/rgame/cli/templates/README.md.tt +93 -0
  126. data/lib/rgame/cli/templates/Rakefile.tt +9 -0
  127. data/lib/rgame/cli/templates/assets/locales/en.yml.tt +10 -0
  128. data/lib/rgame/cli/templates/game.rb.tt +23 -0
  129. data/lib/rgame/cli/templates/gitignore.tt +12 -0
  130. data/lib/rgame/cli/templates/main.rb.tt +11 -0
  131. data/lib/rgame/cli/templates/nodes/root.rb.tt +24 -0
  132. data/lib/rgame/cli/templates/rspec.tt +2 -0
  133. data/lib/rgame/cli/templates/rubocop.yml.tt +75 -0
  134. data/lib/rgame/cli/templates/ruby-version.tt +1 -0
  135. data/lib/rgame/cli/templates/spec/locales_spec.rb.tt +18 -0
  136. data/lib/rgame/cli/templates/spec/nodes/root_spec.rb.tt +18 -0
  137. data/lib/rgame/cli/templates/spec/spec_helper.rb.tt +37 -0
  138. data/lib/rgame/cli.rb +66 -0
  139. data/lib/rgame/core/app.rb +6 -42
  140. data/lib/rgame/core/asset_manager.rb +13 -31
  141. data/lib/rgame/core/audio.rb +37 -16
  142. data/lib/rgame/core/font.rb +0 -3
  143. data/lib/rgame/core/input.rb +35 -41
  144. data/lib/rgame/core/locale.rb +22 -0
  145. data/lib/rgame/core/nine_slice.rb +0 -21
  146. data/lib/rgame/core/recording.rb +3 -1
  147. data/lib/rgame/core/renderer.rb +75 -84
  148. data/lib/rgame/core/sprite_sheet.rb +0 -3
  149. data/lib/rgame/core/tile_map_renderer.rb +77 -65
  150. data/lib/rgame/core/ui_atlas.rb +28 -13
  151. data/lib/rgame/core.rb +1 -8
  152. data/lib/rgame/engine/actor_blockers.rb +131 -0
  153. data/lib/rgame/engine/animation_set.rb +1 -0
  154. data/lib/rgame/engine/audio_director.rb +36 -6
  155. data/lib/rgame/engine/bounds_blockers.rb +74 -0
  156. data/lib/rgame/engine/camera.rb +55 -10
  157. data/lib/rgame/engine/circle_collider.rb +4 -2
  158. data/lib/rgame/engine/collision_box.rb +26 -1
  159. data/lib/rgame/engine/collision_system.rb +110 -22
  160. data/lib/rgame/engine/component.rb +35 -1
  161. data/lib/rgame/engine/components/action_trigger.rb +0 -1
  162. data/lib/rgame/engine/components/animated_sprite.rb +31 -23
  163. data/lib/rgame/engine/components/box_collider.rb +99 -0
  164. data/lib/rgame/engine/components/camera_follow.rb +45 -0
  165. data/lib/rgame/engine/components/character_body.rb +21 -41
  166. data/lib/rgame/engine/components/circle_collider.rb +47 -11
  167. data/lib/rgame/engine/components/collision_world.rb +159 -31
  168. data/lib/rgame/engine/components/despawn_offscreen.rb +24 -8
  169. data/lib/rgame/engine/components/feet_collider.rb +61 -0
  170. data/lib/rgame/engine/components/hop.rb +76 -0
  171. data/lib/rgame/engine/components/identity.rb +73 -0
  172. data/lib/rgame/engine/components/mover.rb +285 -0
  173. data/lib/rgame/engine/components/navigator.rb +145 -0
  174. data/lib/rgame/engine/components/path_follow.rb +123 -31
  175. data/lib/rgame/engine/components/player_controller.rb +5 -2
  176. data/lib/rgame/engine/components/pool.rb +1 -1
  177. data/lib/rgame/engine/components/screen_wrap.rb +33 -11
  178. data/lib/rgame/engine/components/sprite.rb +22 -6
  179. data/lib/rgame/engine/components/targeting.rb +9 -11
  180. data/lib/rgame/engine/components/thrust_controller.rb +1 -1
  181. data/lib/rgame/engine/components/tile_world.rb +70 -29
  182. data/lib/rgame/engine/components/timer.rb +1 -1
  183. data/lib/rgame/engine/components/velocity.rb +23 -7
  184. data/lib/rgame/engine/components/wander_controller.rb +6 -2
  185. data/lib/rgame/engine/components/world.rb +133 -0
  186. data/lib/rgame/engine/contact_set.rb +74 -0
  187. data/lib/rgame/engine/culling.rb +45 -0
  188. data/lib/rgame/engine/debug_overlay.rb +23 -18
  189. data/lib/rgame/engine/i18n/plural.rb +45 -0
  190. data/lib/rgame/engine/i18n/plural_rules.rb +82 -0
  191. data/lib/rgame/engine/i18n/template.rb +59 -0
  192. data/lib/rgame/engine/i18n.rb +276 -51
  193. data/lib/rgame/engine/input/action_mapper.rb +76 -22
  194. data/lib/rgame/engine/input/actions.rb +63 -12
  195. data/lib/rgame/engine/input/input_map.rb +196 -0
  196. data/lib/rgame/engine/layout.rb +82 -0
  197. data/lib/rgame/engine/nav_grid.rb +87 -0
  198. data/lib/rgame/engine/node2d.rb +364 -80
  199. data/lib/rgame/engine/path.rb +4 -6
  200. data/lib/rgame/engine/player.rb +69 -0
  201. data/lib/rgame/engine/player_layer.rb +70 -0
  202. data/lib/rgame/engine/players.rb +205 -0
  203. data/lib/rgame/engine/presentation.rb +171 -0
  204. data/lib/rgame/engine/scene/scene_stack.rb +29 -7
  205. data/lib/rgame/engine/sealed_privates.rb +54 -0
  206. data/lib/rgame/engine/spatial_hash.rb +53 -8
  207. data/lib/rgame/engine/text.rb +194 -0
  208. data/lib/rgame/engine/tile_blockers.rb +63 -0
  209. data/lib/rgame/engine/tile_map.rb +2 -3
  210. data/lib/rgame/engine/tile_map_layer.rb +82 -0
  211. data/lib/rgame/engine/tileset.rb +2 -4
  212. data/lib/rgame/engine/timer.rb +2 -2
  213. data/lib/rgame/engine/ui/button.rb +248 -0
  214. data/lib/rgame/engine/ui/column.rb +20 -0
  215. data/lib/rgame/engine/ui/icon_button.rb +93 -0
  216. data/lib/rgame/engine/ui/menu.rb +290 -0
  217. data/lib/rgame/engine/ui/navigation.rb +57 -0
  218. data/lib/rgame/engine/ui/nine_slice_style.rb +50 -0
  219. data/lib/rgame/engine/ui/option_button.rb +163 -0
  220. data/lib/rgame/engine/ui/panel_button.rb +32 -0
  221. data/lib/rgame/engine/ui/panel_menu.rb +36 -0
  222. data/lib/rgame/engine/ui/pointing.rb +146 -0
  223. data/lib/rgame/engine/ui/radial_menu.rb +85 -0
  224. data/lib/rgame/engine/ui/ring.rb +55 -0
  225. data/lib/rgame/engine/ui/row.rb +21 -0
  226. data/lib/rgame/engine/ui/shape_style.rb +102 -0
  227. data/lib/rgame/engine/ui/stack.rb +58 -0
  228. data/lib/rgame/engine/ui/stepping.rb +93 -0
  229. data/lib/rgame/engine/ui/text_button.rb +59 -0
  230. data/lib/rgame/engine/view.rb +76 -0
  231. data/lib/rgame/engine/viewports.rb +171 -0
  232. data/lib/rgame/engine/world_view.rb +71 -0
  233. data/lib/rgame/engine.rb +43 -25
  234. data/lib/rgame/game.rb +164 -22
  235. data/lib/rgame/rubocop/cop/game/draw_in_local_space.rb +103 -0
  236. data/lib/rgame/rubocop/cop/game/hot_path.rb +36 -0
  237. data/lib/rgame/rubocop/cop/game/layer_boundary.rb +43 -0
  238. data/lib/rgame/rubocop/cop/game/no_core_in_engine_layer.rb +100 -0
  239. data/lib/rgame/rubocop/cop/game/no_engine_in_core_layer.rb +84 -0
  240. data/lib/rgame/rubocop/cop/game/no_interpolation_in_hot_path.rb +50 -0
  241. data/lib/rgame/rubocop/cop/game/no_literal_text.rb +41 -0
  242. data/lib/rgame/rubocop/cop/game/no_needless_allocation.rb +112 -0
  243. data/lib/rgame/rubocop/default.yml +39 -0
  244. data/lib/rgame/rubocop/plugin.rb +45 -0
  245. data/lib/rgame/rubocop.rb +11 -0
  246. data/lib/rgame/util/color.rb +20 -24
  247. data/lib/rgame/util/controls.rb +106 -44
  248. data/lib/rgame/util/route_search.rb +27 -0
  249. data/lib/rgame/util/save_file.rb +107 -0
  250. data/lib/rgame/util/solid_grid.rb +37 -0
  251. data/lib/rgame/util/tensor.rb +0 -9
  252. data/lib/rgame/util/tile_sweep.rb +36 -0
  253. data/lib/rgame/util/z.rb +123 -0
  254. data/lib/rgame/util.rb +5 -3
  255. data/lib/rgame/version.rb +1 -1
  256. data/lib/rgame.rb +0 -15
  257. metadata +176 -20
  258. data/lib/rgame/engine/actor.rb +0 -53
  259. data/lib/rgame/engine/body.rb +0 -49
  260. data/lib/rgame/engine/cached_label.rb +0 -33
  261. data/lib/rgame/engine/camera_view.rb +0 -28
  262. data/lib/rgame/engine/input/player_controller.rb +0 -14
  263. data/lib/rgame/engine/matrix.rb +0 -32
  264. data/lib/rgame/engine/resettable.rb +0 -67
  265. data/lib/rgame/engine/tile_collision.rb +0 -78
@@ -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,
@@ -5,72 +5,66 @@ require_relative '../util/controls'
5
5
 
6
6
  module RGame
7
7
  module Core
8
- # Translates the game's symbolic actions (:fire, :confirm) into the physical
9
- # button ids the engine understands, and asks the app whether they're held.
8
+ # Asks the engine whether a physical input is active, on a given device.
10
9
  #
11
10
  # input = RGame::Core::Input.new(app)
12
- # input.down?(:fire) # keyboard, the single-player default
13
- # input.down?(:fire, device: 1) # the pad in player slot 0
14
- # input.axis(:move_x, device: 1) # => Float, -1.0..1.0
11
+ # input.down?(Controls::KEY_SPACE) # keyboard
12
+ # input.down?(Controls::PAD_A, device: Controls.gamepad(0)) # player 1's pad
13
+ # input.axis(Controls::AXIS_LEFT_X, device: Controls.gamepad(0))
15
14
  #
16
- # The id vocabulary itself lives in RGame::Util::Controls, not here: ids are
17
- # values, and the engine layer must be able to name one without touching
18
- # Core. That is also what makes rebinding possible a game builds its own
19
- # table from those constants and passes it in:
15
+ # **This is the raw query, and deliberately nothing more.** It used to carry
16
+ # binding tables of its own `down?(:fire)` resolved `:fire` to a scancode
17
+ # through one of three tables passed to the constructor. Those are gone, and
18
+ # binding now lives one layer up in RGame::Engine::InputMap, for two
19
+ # reasons. A game's rebinding screen has to be able to edit the table, and
20
+ # the engine layer may not name RGame::Core at all; and with a player per
21
+ # device, the table is a per-player value rather than a property of the one
22
+ # object that talks to the hardware.
20
23
  #
21
- # controls = RGame::Util::Controls
22
- # input = RGame::Core::Input.new(
23
- # app, bindings: controls::DEFAULT_KEYBOARD.merge(fire: controls::KEY_J)
24
- # )
24
+ # What is left is an argument-order adapter over the app's own
25
+ # `input_down?(device, id)` / `input_axis(device, axis_id)`. It stays a named
26
+ # class rather than collapsing into those, because it is what gets handed to
27
+ # the engine layer as an input backend — and handing it a whole App, which
28
+ # can also close the window and rename it, would be a worse seam.
25
29
  #
26
- # Devices are numbered keyboard-first: Controls::KEYBOARD is 0, and
27
- # Controls.gamepad(slot) is the pad in that player slot. Defaulting
28
- # `device:` to the keyboard is what lets single-player call sites stay
29
- # `input.down?(:fire)` with no ceremony.
30
+ # Ids come from RGame::Util::Controls. They are values with nothing behind
31
+ # them, so both layers can name one.
30
32
  #
31
33
  # **There is no pointer or mouse support, by design.** The layer this
32
34
  # replaced had a cursor position and a click button riding the same "is
33
35
  # held" path as keys, and none of it was carried over: this engine's input
34
- # is keyboard and controllers. A game wanting click-based UI has to build
35
- # hit-testing on top rather than find it here, and the intended answer for
36
- # menus is keyboard and controller navigation instead.
36
+ # is keyboard and controllers. The intended answer for menus is keyboard and
37
+ # controller navigation instead.
37
38
  class Input
38
39
  Controls = RGame::Util::Controls
39
40
 
40
- # `bindings` covers the keyboard, `pad_bindings` the controllers, and
41
- # `axis_bindings` the analog axes. All default to the standard tables.
42
- def initialize(app,
43
- bindings: Controls::DEFAULT_KEYBOARD,
44
- pad_bindings: Controls::DEFAULT_PAD,
45
- axis_bindings: Controls::DEFAULT_AXES)
41
+ def initialize(app)
46
42
  @app = app
47
- @bindings = bindings
48
- @pad_bindings = pad_bindings
49
- @axis_bindings = axis_bindings
50
43
  end
51
44
 
52
- # Is the action's physical button held on `device`?
45
+ # Is `id` held on `device`?
53
46
  #
54
47
  # Reads the engine's per-frame input snapshot, so the answer is identical
55
48
  # for every simulation tick within one frame — a key held for a single
56
49
  # frame behaves the same whether that frame ran one catch-up tick or five.
50
+ #
51
+ # A device only answers for its own kind of input: asking a gamepad about
52
+ # a keyboard scancode is `false`, never the keyboard's answer. That is
53
+ # what lets one binding table list a key and a pad button for the same
54
+ # action and still keep player two's pad from echoing player one.
57
55
  # hot-path
58
- def down?(action, device: Controls::KEYBOARD)
59
- @app.input_down?(device, bindings_for(device).fetch(action))
56
+ def down?(id, device: Controls::KEYBOARD)
57
+ @app.input_down?(device, id)
60
58
  end
61
59
 
62
60
  # Current value of an analog axis: sticks -1.0..1.0, triggers 0.0..1.0.
63
61
  # The keyboard has no axes, so it always reads 0.0.
62
+ #
63
+ # No dead zone is applied here — this is the hardware's answer. Ignoring
64
+ # a resting stick's jitter is RGame::Engine::ActionMapper's job.
64
65
  # hot-path
65
- def axis(action, device: Controls::KEYBOARD)
66
- @app.input_axis(device, @axis_bindings.fetch(action))
67
- end
68
-
69
- private
70
-
71
- # hot-path
72
- def bindings_for(device)
73
- device == Controls::KEYBOARD ? @bindings : @pad_bindings
66
+ def axis(axis_id, device: Controls::KEYBOARD)
67
+ @app.input_axis(device, axis_id)
74
68
  end
75
69
  end
76
70
  end
@@ -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
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'rgame/core_ext'
4
4
  require_relative '../util/color'
5
+ require_relative '../util/z'
5
6
 
6
7
  module RGame
7
8
  module Core
@@ -37,6 +38,7 @@ module RGame
37
38
  # renderer.clipped(0, 0, 400, 600) { ground.draw(0, 0) }
38
39
  class Recording
39
40
  Color = RGame::Util::Color
41
+ Z = RGame::Util::Z
40
42
 
41
43
  # Replays everything that was recorded, with the recording's origin at
42
44
  # (x, y).
@@ -45,7 +47,7 @@ module RGame
45
47
  # so white (the default) draws the recording unchanged and a colour with
46
48
  # alpha fades the whole layer out at once.
47
49
  def draw(x = 0, y = 0, z: 0, color: nil)
48
- draw_at(x, y, z, Color.coerce(color).packed)
50
+ draw_at(x, y, Z.offset(z), Color.coerce(color).packed)
49
51
  end
50
52
  end
51
53
  end