rgame 0.4.0-x86_64-linux-gnu

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 (239) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +251 -0
  3. data/LICENSE +26 -0
  4. data/README.md +325 -0
  5. data/docs/api/README.md +224 -0
  6. data/docs/api/app.md +249 -0
  7. data/docs/api/assets.md +463 -0
  8. data/docs/api/audio.md +249 -0
  9. data/docs/api/cli.md +259 -0
  10. data/docs/api/components.md +1112 -0
  11. data/docs/api/drawing.md +398 -0
  12. data/docs/api/examples.md +263 -0
  13. data/docs/api/game.md +219 -0
  14. data/docs/api/images.md +118 -0
  15. data/docs/api/input.md +476 -0
  16. data/docs/api/internals.md +289 -0
  17. data/docs/api/localization.md +285 -0
  18. data/docs/api/scene_graph.md +539 -0
  19. data/docs/api/signals.md +147 -0
  20. data/docs/api/systems.md +231 -0
  21. data/docs/api/text.md +133 -0
  22. data/docs/api/tile_maps.md +237 -0
  23. data/docs/api/toolbox.md +493 -0
  24. data/docs/api/ui.md +927 -0
  25. data/docs/api/values.md +316 -0
  26. data/examples/assets/README.md +322 -0
  27. data/examples/assets/blip.ogg +0 -0
  28. data/examples/assets/glyphs.json +5 -0
  29. data/examples/assets/glyphs.png +0 -0
  30. data/examples/assets/hero.json +12 -0
  31. data/examples/assets/hero.png +0 -0
  32. data/examples/assets/icons.json +13 -0
  33. data/examples/assets/icons.png +0 -0
  34. data/examples/assets/music.ogg +0 -0
  35. data/examples/assets/skills.json +10 -0
  36. data/examples/assets/skills.png +0 -0
  37. data/examples/assets/tileset.png +0 -0
  38. data/examples/assets/tileset.tsx +65 -0
  39. data/examples/assets/town.tmx +26 -0
  40. data/examples/assets/ui.json +11 -0
  41. data/examples/assets/ui.png +0 -0
  42. data/examples/collision/locales/en.yml +8 -0
  43. data/examples/collision/main.rb +316 -0
  44. data/examples/collision_tiles/locales/en.yml +9 -0
  45. data/examples/collision_tiles/main.rb +274 -0
  46. data/examples/fullscreen/locales/en.yml +10 -0
  47. data/examples/fullscreen/main.rb +216 -0
  48. data/examples/game_menu/locales/en.yml +8 -0
  49. data/examples/game_menu/main.rb +170 -0
  50. data/examples/input_glyphs/locales/en.yml +14 -0
  51. data/examples/input_glyphs/main.rb +213 -0
  52. data/examples/jump_topdown/locales/en.yml +9 -0
  53. data/examples/jump_topdown/main.rb +178 -0
  54. data/examples/localization/locales/de.yml +12 -0
  55. data/examples/localization/locales/en.yml +13 -0
  56. data/examples/localization/main.rb +158 -0
  57. data/examples/menu_navigation/locales/en.yml +23 -0
  58. data/examples/menu_navigation/main.rb +365 -0
  59. data/examples/music/locales/en.yml +7 -0
  60. data/examples/music/main.rb +134 -0
  61. data/examples/pathfinding/locales/en.yml +17 -0
  62. data/examples/pathfinding/main.rb +298 -0
  63. data/examples/pooling/locales/en.yml +7 -0
  64. data/examples/pooling/main.rb +259 -0
  65. data/examples/quick_wheel/locales/en.yml +16 -0
  66. data/examples/quick_wheel/main.rb +184 -0
  67. data/examples/radial_menu/locales/en.yml +16 -0
  68. data/examples/radial_menu/main.rb +184 -0
  69. data/examples/save_load/locales/en.yml +11 -0
  70. data/examples/save_load/main.rb +207 -0
  71. data/examples/save_load_ids/locales/en.yml +11 -0
  72. data/examples/save_load_ids/main.rb +322 -0
  73. data/examples/scroll_map/locales/en.yml +4 -0
  74. data/examples/scroll_map/main.rb +140 -0
  75. data/examples/signals/locales/en.yml +6 -0
  76. data/examples/signals/main.rb +278 -0
  77. data/examples/skill_bar/locales/en.yml +14 -0
  78. data/examples/skill_bar/main.rb +159 -0
  79. data/examples/sound/locales/en.yml +6 -0
  80. data/examples/sound/main.rb +122 -0
  81. data/examples/split_screen/locales/en.yml +9 -0
  82. data/examples/split_screen/main.rb +304 -0
  83. data/examples/sprite/locales/en.yml +8 -0
  84. data/examples/sprite/main.rb +180 -0
  85. data/examples/timer/locales/en.yml +12 -0
  86. data/examples/timer/main.rb +273 -0
  87. data/examples/velocity/locales/en.yml +6 -0
  88. data/examples/velocity/main.rb +196 -0
  89. data/examples/walk/locales/en.yml +4 -0
  90. data/examples/walk/main.rb +99 -0
  91. data/exe/rgame +9 -0
  92. data/lib/rgame/boot.rb +3 -0
  93. data/lib/rgame/cli/new_project.rb +139 -0
  94. data/lib/rgame/cli/templates/Gemfile.tt +23 -0
  95. data/lib/rgame/cli/templates/README.md.tt +93 -0
  96. data/lib/rgame/cli/templates/Rakefile.tt +9 -0
  97. data/lib/rgame/cli/templates/assets/locales/en.yml.tt +10 -0
  98. data/lib/rgame/cli/templates/game.rb.tt +23 -0
  99. data/lib/rgame/cli/templates/gitignore.tt +12 -0
  100. data/lib/rgame/cli/templates/main.rb.tt +11 -0
  101. data/lib/rgame/cli/templates/nodes/root.rb.tt +24 -0
  102. data/lib/rgame/cli/templates/rspec.tt +2 -0
  103. data/lib/rgame/cli/templates/rubocop.yml.tt +75 -0
  104. data/lib/rgame/cli/templates/ruby-version.tt +1 -0
  105. data/lib/rgame/cli/templates/spec/locales_spec.rb.tt +18 -0
  106. data/lib/rgame/cli/templates/spec/nodes/root_spec.rb.tt +18 -0
  107. data/lib/rgame/cli/templates/spec/spec_helper.rb.tt +37 -0
  108. data/lib/rgame/cli.rb +66 -0
  109. data/lib/rgame/core/app.rb +46 -0
  110. data/lib/rgame/core/asset_manager.rb +206 -0
  111. data/lib/rgame/core/audio.rb +145 -0
  112. data/lib/rgame/core/font.rb +46 -0
  113. data/lib/rgame/core/gamepad.rb +55 -0
  114. data/lib/rgame/core/image.rb +55 -0
  115. data/lib/rgame/core/input.rb +71 -0
  116. data/lib/rgame/core/locale.rb +22 -0
  117. data/lib/rgame/core/nine_slice.rb +142 -0
  118. data/lib/rgame/core/recording.rb +54 -0
  119. data/lib/rgame/core/renderer.rb +354 -0
  120. data/lib/rgame/core/sprite_sheet.rb +105 -0
  121. data/lib/rgame/core/tile_map_renderer.rb +172 -0
  122. data/lib/rgame/core/ui_atlas.rb +101 -0
  123. data/lib/rgame/core/virtual_gamepad.rb +26 -0
  124. data/lib/rgame/core.rb +18 -0
  125. data/lib/rgame/core_ext.so +0 -0
  126. data/lib/rgame/engine/actor_blockers.rb +131 -0
  127. data/lib/rgame/engine/animation_set.rb +50 -0
  128. data/lib/rgame/engine/animator.rb +44 -0
  129. data/lib/rgame/engine/audio_bus.rb +24 -0
  130. data/lib/rgame/engine/audio_director.rb +59 -0
  131. data/lib/rgame/engine/bounds_blockers.rb +74 -0
  132. data/lib/rgame/engine/camera.rb +78 -0
  133. data/lib/rgame/engine/circle_collider.rb +34 -0
  134. data/lib/rgame/engine/collision_box.rb +59 -0
  135. data/lib/rgame/engine/collision_system.rb +132 -0
  136. data/lib/rgame/engine/component.rb +64 -0
  137. data/lib/rgame/engine/components/action_trigger.rb +40 -0
  138. data/lib/rgame/engine/components/animated_sprite.rb +71 -0
  139. data/lib/rgame/engine/components/box_collider.rb +99 -0
  140. data/lib/rgame/engine/components/camera_follow.rb +45 -0
  141. data/lib/rgame/engine/components/character_body.rb +50 -0
  142. data/lib/rgame/engine/components/circle_collider.rb +80 -0
  143. data/lib/rgame/engine/components/collision_world.rb +231 -0
  144. data/lib/rgame/engine/components/despawn_offscreen.rb +42 -0
  145. data/lib/rgame/engine/components/feet_collider.rb +61 -0
  146. data/lib/rgame/engine/components/hop.rb +76 -0
  147. data/lib/rgame/engine/components/identity.rb +73 -0
  148. data/lib/rgame/engine/components/mover.rb +285 -0
  149. data/lib/rgame/engine/components/navigator.rb +145 -0
  150. data/lib/rgame/engine/components/path_follow.rb +176 -0
  151. data/lib/rgame/engine/components/player_controller.rb +27 -0
  152. data/lib/rgame/engine/components/pool.rb +53 -0
  153. data/lib/rgame/engine/components/screen_wrap.rb +49 -0
  154. data/lib/rgame/engine/components/sprite.rb +47 -0
  155. data/lib/rgame/engine/components/targeting.rb +52 -0
  156. data/lib/rgame/engine/components/thrust_controller.rb +65 -0
  157. data/lib/rgame/engine/components/tile_world.rb +109 -0
  158. data/lib/rgame/engine/components/timer.rb +75 -0
  159. data/lib/rgame/engine/components/velocity.rb +43 -0
  160. data/lib/rgame/engine/components/wander_controller.rb +64 -0
  161. data/lib/rgame/engine/components/world.rb +133 -0
  162. data/lib/rgame/engine/contact_set.rb +74 -0
  163. data/lib/rgame/engine/culling.rb +45 -0
  164. data/lib/rgame/engine/debug_overlay.rb +111 -0
  165. data/lib/rgame/engine/i18n/plural.rb +45 -0
  166. data/lib/rgame/engine/i18n/plural_rules.rb +82 -0
  167. data/lib/rgame/engine/i18n/template.rb +59 -0
  168. data/lib/rgame/engine/i18n.rb +322 -0
  169. data/lib/rgame/engine/input/action_mapper.rb +100 -0
  170. data/lib/rgame/engine/input/actions.rb +92 -0
  171. data/lib/rgame/engine/input/input_map.rb +196 -0
  172. data/lib/rgame/engine/layout.rb +82 -0
  173. data/lib/rgame/engine/nav_grid.rb +87 -0
  174. data/lib/rgame/engine/node2d.rb +555 -0
  175. data/lib/rgame/engine/path.rb +76 -0
  176. data/lib/rgame/engine/player.rb +69 -0
  177. data/lib/rgame/engine/player_layer.rb +70 -0
  178. data/lib/rgame/engine/players.rb +205 -0
  179. data/lib/rgame/engine/pool.rb +51 -0
  180. data/lib/rgame/engine/presentation.rb +171 -0
  181. data/lib/rgame/engine/scene/scene_stack.rb +87 -0
  182. data/lib/rgame/engine/sealed_privates.rb +54 -0
  183. data/lib/rgame/engine/signal.rb +75 -0
  184. data/lib/rgame/engine/spatial_hash.rb +116 -0
  185. data/lib/rgame/engine/text.rb +194 -0
  186. data/lib/rgame/engine/tile_blockers.rb +63 -0
  187. data/lib/rgame/engine/tile_map.rb +147 -0
  188. data/lib/rgame/engine/tile_map_layer.rb +82 -0
  189. data/lib/rgame/engine/tileset.rb +99 -0
  190. data/lib/rgame/engine/timer.rb +51 -0
  191. data/lib/rgame/engine/ui/button.rb +248 -0
  192. data/lib/rgame/engine/ui/column.rb +20 -0
  193. data/lib/rgame/engine/ui/icon_button.rb +93 -0
  194. data/lib/rgame/engine/ui/menu.rb +290 -0
  195. data/lib/rgame/engine/ui/navigation.rb +57 -0
  196. data/lib/rgame/engine/ui/nine_slice_style.rb +50 -0
  197. data/lib/rgame/engine/ui/option_button.rb +163 -0
  198. data/lib/rgame/engine/ui/panel_button.rb +32 -0
  199. data/lib/rgame/engine/ui/panel_menu.rb +36 -0
  200. data/lib/rgame/engine/ui/pointing.rb +146 -0
  201. data/lib/rgame/engine/ui/radial_menu.rb +85 -0
  202. data/lib/rgame/engine/ui/ring.rb +55 -0
  203. data/lib/rgame/engine/ui/row.rb +21 -0
  204. data/lib/rgame/engine/ui/shape_style.rb +102 -0
  205. data/lib/rgame/engine/ui/stack.rb +58 -0
  206. data/lib/rgame/engine/ui/stepping.rb +93 -0
  207. data/lib/rgame/engine/ui/text_button.rb +59 -0
  208. data/lib/rgame/engine/view.rb +76 -0
  209. data/lib/rgame/engine/viewports.rb +171 -0
  210. data/lib/rgame/engine/world_view.rb +71 -0
  211. data/lib/rgame/engine.rb +86 -0
  212. data/lib/rgame/fonts/LiberationSans-Regular.ttf +0 -0
  213. data/lib/rgame/fonts/OFL.txt +102 -0
  214. data/lib/rgame/game.rb +271 -0
  215. data/lib/rgame/rubocop/cop/game/draw_in_local_space.rb +103 -0
  216. data/lib/rgame/rubocop/cop/game/hot_path.rb +36 -0
  217. data/lib/rgame/rubocop/cop/game/layer_boundary.rb +43 -0
  218. data/lib/rgame/rubocop/cop/game/no_core_in_engine_layer.rb +100 -0
  219. data/lib/rgame/rubocop/cop/game/no_engine_in_core_layer.rb +84 -0
  220. data/lib/rgame/rubocop/cop/game/no_interpolation_in_hot_path.rb +50 -0
  221. data/lib/rgame/rubocop/cop/game/no_literal_text.rb +41 -0
  222. data/lib/rgame/rubocop/cop/game/no_needless_allocation.rb +112 -0
  223. data/lib/rgame/rubocop/default.yml +39 -0
  224. data/lib/rgame/rubocop/plugin.rb +45 -0
  225. data/lib/rgame/rubocop.rb +11 -0
  226. data/lib/rgame/util/color.rb +23 -0
  227. data/lib/rgame/util/controls.rb +169 -0
  228. data/lib/rgame/util/route_search.rb +27 -0
  229. data/lib/rgame/util/save_file.rb +107 -0
  230. data/lib/rgame/util/solid_grid.rb +37 -0
  231. data/lib/rgame/util/tensor.rb +3 -0
  232. data/lib/rgame/util/tile_sweep.rb +36 -0
  233. data/lib/rgame/util/z.rb +123 -0
  234. data/lib/rgame/util.rb +10 -0
  235. data/lib/rgame/util_ext.so +0 -0
  236. data/lib/rgame/version.rb +12 -0
  237. data/lib/rgame.rb +5 -0
  238. data/licenses/SDL2/LICENSE.txt +18 -0
  239. metadata +309 -0
data/docs/api/audio.md ADDED
@@ -0,0 +1,249 @@
1
+ # Audio
2
+
3
+ rgame plays sound through three classes. `Audio` is the sound device. A `Sample`
4
+ is a short sound that can play over itself. A `Song` is a long sound streamed
5
+ from disk.
6
+
7
+ **A scene in an `RGame::Game` never touches these classes.** It emits on
8
+ [`AudioBus`](toolbox.md#audiobus--decoupled-audio-facts), for example
9
+ `RGame::Engine::AudioBus.play_sound('hit.ogg')`, and the game's director plays it.
10
+ The example below drives the device directly from a plain `App`:
11
+
12
+ ```ruby
13
+ require 'rgame/core'
14
+
15
+ class MyGame < RGame::Core::App
16
+ def initialize
17
+ super(width: 800, height: 600, caption: 'demo')
18
+ @hit = audio.sample('assets/hit.ogg')
19
+ @music = audio.song('assets/theme.ogg')
20
+ @music.play(looping: true)
21
+ end
22
+
23
+ def button_down(id)
24
+ @hit.play if id == RGame::Util::Controls::KEY_SPACE
25
+ end
26
+ end
27
+
28
+ MyGame.new.run
29
+ ```
30
+
31
+ rgame decodes Ogg Vorbis and WAV, and nothing else. See
32
+ [What is not here](#what-is-not-here).
33
+
34
+ ## The device
35
+
36
+ ```ruby
37
+ audio = app.audio # the one a game uses
38
+ audio.backend # => "PulseAudio"
39
+ audio.volume # => 1.0
40
+ audio.volume = 0.8
41
+ ```
42
+
43
+ **Use `App#audio`.** An app opens the device on first use and hands it the app's
44
+ asset manager. That manager lets `play_sound('hurt.ogg')` name a file.
45
+ `RGame::Core::Audio.new` builds a standalone device for a tool or a spec. It has
46
+ no manager, so it plays only objects it receives or has registered.
47
+
48
+ **The device is not tied to a window.** Unlike an `Image`, a sound belongs to no
49
+ GL context. It survives a window resize or rebuild, and `Audio.new` takes no app.
50
+ An app holds one device only because a program wants exactly one, like its asset
51
+ manager.
52
+
53
+ **A machine without sound hardware still gets a working device.** It opens a
54
+ null backend and plays silence instead of raising, and `#backend` returns
55
+ `"Null"`. A game should run on a CI runner, in a container, or on a laptop with
56
+ its sound card off. Crashing at startup over sound would be the worse failure.
57
+ Ask `#backend` if your game needs to know; nothing else changes.
58
+
59
+ `#volume` is the master volume. The device multiplies it into everything it
60
+ plays.
61
+
62
+ ## Samples
63
+
64
+ ```ruby
65
+ hit = audio.sample('assets/hit.ogg')
66
+ hit.volume = 0.5
67
+ hit.play
68
+ hit.play # a second voice, over the first
69
+ ```
70
+
71
+ A sample decodes once, into memory, and plays as often as you like. **Playing a
72
+ sample that is already sounding layers another voice over it.** It does not
73
+ restart. A fast run of footsteps therefore sounds like footsteps, not one
74
+ stuttering step.
75
+
76
+ A sample is fire-and-forget. A single play has no handle and cannot be stopped.
77
+ Volume belongs to the sample and reaches every voice it has out, including those
78
+ already sounding.
79
+
80
+ **Use samples for short sounds only.** A sample holds its whole decoded length in
81
+ memory, roughly 10 MB per minute of CD-quality stereo. Put music in a `Song`.
82
+
83
+ Two samples loaded from the **same path** share one decoded copy. Loading a file
84
+ a second time costs nothing.
85
+
86
+ ## Songs
87
+
88
+ ```ruby
89
+ music = audio.song('assets/theme.ogg')
90
+ music.play(looping: true)
91
+ music.playing? # => true
92
+ music.volume = 0.6
93
+ music.stop
94
+ ```
95
+
96
+ A song streams from its file as it plays. A three-minute track costs a buffer,
97
+ not forty megabytes.
98
+
99
+ **A song is one voice.** Playing a song that is already playing restarts it from
100
+ the beginning, and so does playing it after `stop`. Songs cannot pause: `stop`
101
+ then `play` starts from the top.
102
+
103
+ `#playing?` and `#looping?` report what the song was last told to do.
104
+ `#looping?` is a flag, not a count.
105
+
106
+ **The engine allows several songs at once**; a crossfade needs two. A game that
107
+ wants one song at a time stops the old one before starting the next.
108
+
109
+ ## Playing by id
110
+
111
+ **Gameplay names a sound; it does not hold one.** Drawing follows the same rule,
112
+ because a scene may not hold a `Sample`. `examples/sound` and `examples/music`
113
+ show both kinds. The first fires and layers a sample from a button. The second
114
+ loops, stops and restarts a song.
115
+
116
+ ```ruby
117
+ audio.play_sound('hurt.ogg')
118
+ audio.play_music('theme.ogg') # loops
119
+ audio.stop_music
120
+ ```
121
+
122
+ **Ids come in two kinds, the same two the renderer uses.** A **String is a
123
+ root-relative path**. The device resolves it through the asset manager on first
124
+ use and remembers the result. A **Symbol is a name the game chose**, and only
125
+ registration binds one:
126
+
127
+ ```ruby
128
+ audio.register_sound(:hit, app.assets.sound('hurt.ogg'))
129
+ audio.register_music(:theme, app.assets.song('theme.ogg'))
130
+ audio.play_sound(:hit)
131
+ audio.play_music(:theme)
132
+ ```
133
+
134
+ Registration also *overrides* a path. A game uses that to bind a sound it built
135
+ instead of loaded. An id that is neither registered nor resolvable raises
136
+ `KeyError`. `nil` raises `TypeError`, because an asset that resolved to nothing
137
+ is a different bug from a mistyped name.
138
+
139
+ **The device caches every resolution, and must.** `play_music` asks the song
140
+ whether it is already playing. Resolving one path to two `Song` objects would
141
+ defeat that check and restart the track on every request.
142
+
143
+ **A game wires none of this.** When `RGame::Game` starts, it subscribes an
144
+ [`AudioDirector`](toolbox.md) to the global `AudioBus`. When the loop ends, it
145
+ unsubscribes it. A scene that emits on the bus is heard with no setup. The engine
146
+ owns both steps because each failure is invisible. Without a director, the tree
147
+ runs and the events fire, but nothing plays. A director left on the bus keeps
148
+ the device, the asset manager and the whole `App` alive for the life of the
149
+ process. A single game never notices, but a process running two games does.
150
+
151
+ **`play_music` is idempotent.** Asking for the track already playing does
152
+ nothing. A scene that repeats the request each time it is entered never restarts
153
+ the music mid-loop.
154
+
155
+ **`stop_music` stops the song `play_music` most recently started**, not whatever
156
+ is sounding. The engine keeps no process-wide "current song", because one song at
157
+ a time is a game's policy. **`play_music` with a different track does not stop the
158
+ previous one**, so both play. To switch tracks, call `stop_music` first. You stop
159
+ a `Song` you started by hand yourself. `stop_music` with nothing playing does
160
+ nothing.
161
+
162
+ ## Loading and failure
163
+
164
+ ```ruby
165
+ audio.sample(path) # => RGame::Core::Sample
166
+ audio.song(path) # => RGame::Core::Song
167
+
168
+ RGame::Core::Sample.new(audio, path) # the same thing
169
+ RGame::Core::Song.new(audio, path)
170
+ ```
171
+
172
+ **Prefer `audio.sample` and `audio.song`.** They read in the direction the
173
+ objects depend, and a stand-in device can offer them, while `Sample.new` cannot
174
+ be faked (see [Testing](#testing)).
175
+
176
+ `audio.sample`, `audio.song` and the two constructors resolve a relative path
177
+ against the working directory, and cache nothing. `app.assets.sound(path)` and
178
+ `app.assets.song(path)` resolve against `media_root` and cache, and a String id
179
+ passed to `play_sound` or `play_music` goes through them.
180
+
181
+ A file the engine cannot read or decode raises `RGame::Core::Sample::LoadError`
182
+ or `RGame::Core::Song::LoadError`, naming the file. Both inherit from
183
+ `StandardError`, so an ordinary `rescue` catches them:
184
+
185
+ ```ruby
186
+ @music = begin
187
+ audio.song('assets/theme.ogg')
188
+ rescue RGame::Core::Song::LoadError => e
189
+ warn "no music: #{e.message}"
190
+ nil
191
+ end
192
+ ```
193
+
194
+ The engine checks the **content, not the extension**. It refuses a text file
195
+ named `.ogg`.
196
+
197
+ ## Volume
198
+
199
+ Every volume behaves the same way, whether the device's, a sample's or a song's:
200
+
201
+ | Value | Effect |
202
+ |---|---|
203
+ | `1.0` | unchanged, the default |
204
+ | `0.0` … `1.0` | quieter |
205
+ | above `1.0` | amplified; clipping is yours to avoid |
206
+ | below `0.0` | clamped to `0.0` |
207
+
208
+ **A negative volume clamps to silence instead of raising.** A fader driven by a
209
+ slider or an easing curve undershoots all the time, and silence is the useful
210
+ answer. A true negative volume would invert the phase, which sounds *louder*.
211
+
212
+ The mixer stores volumes as 32-bit floats, so `0.8` reads back as
213
+ `0.800000011920929`. Compare with a tolerance, not with `==`.
214
+
215
+ ## What it costs
216
+
217
+ **Nothing needs freeing.** A sample or song releases its memory when it is
218
+ collected. Each keeps its device alive while it exists. Dropping your reference
219
+ to the `Audio` while a sound remains is safe, in either order.
220
+
221
+ `RGame::Core::Audio.debug_live_sounds` returns how many samples and songs exist.
222
+ It serves tests, not gameplay.
223
+
224
+ ## Testing
225
+
226
+ **Audio follows the drawing pattern.** The engine layer receives a device and
227
+ calls it by method name, never by class. A headless spec can substitute a device
228
+ that makes no sound and records every call.
229
+
230
+ rgame's own suite uses `spec/support/fake_audio.rb` as that stand-in.
231
+ `spec/support/shared_examples/an_audio_server.rb` defines the contract, and both
232
+ the fake and the real device run against it. A fake that drifted from the device
233
+ would keep `rake spec` green while the game played nothing.
234
+
235
+ ```ruby
236
+ audio = FakeAudio.new
237
+ audio.sample('hit.ogg').play
238
+
239
+ expect(audio.played?('hit.ogg')).to be(true)
240
+ expect(audio.calls.map(&:name)).to eq(%i[sample sample_play])
241
+ ```
242
+
243
+ That spec loads no file, opens no device and needs no sound card.
244
+
245
+ ## What is not here
246
+
247
+ rgame audio has no MP3 or FLAC; it decodes Vorbis and WAV only, to keep the gem
248
+ small. It also lacks positional and 3D audio, effects and filters, fades,
249
+ pausing, seeking, per-play handles, playback position and recording.
data/docs/api/cli.md ADDED
@@ -0,0 +1,259 @@
1
+ # The `rgame` command
2
+
3
+ Installing the gem puts one command on your PATH.
4
+
5
+ ```
6
+ gem install rgame
7
+ rgame new tictactoe
8
+ ```
9
+
10
+ | Command | Does |
11
+ |---|---|
12
+ | `rgame new NAME` | Creates the directory `NAME` and writes a runnable project into it |
13
+ | `rgame version` | Prints the installed engine version |
14
+ | `rgame help` | Prints usage |
15
+
16
+ `rgame new` accepts a name made of letters, digits, underscores and dashes,
17
+ starting with a letter, and refuses anything else. It also refuses a
18
+ path that exists and is not a directory, and a directory that holds anything. It
19
+ writes into an existing *empty* directory.
20
+
21
+ ## What `rgame new tictactoe` writes
22
+
23
+ ```
24
+ tictactoe/
25
+ ├── Gemfile rgame, plus rspec and rubocop for development
26
+ ├── Rakefile rake spec, rake rubocop, rake
27
+ ├── README.md
28
+ ├── .ruby-version the Ruby that ran `rgame new`
29
+ ├── .gitignore .rspec .rubocop.yml
30
+ ├── main.rb boots the game and nothing else
31
+ ├── game.rb class TictactoeGame < RGame::Game
32
+ ├── assets/ the game's media_root
33
+ │ └── locales/
34
+ │ └── en.yml the English translation table
35
+ ├── nodes/
36
+ │ └── root.rb class Root < RGame::Engine::Node2D
37
+ └── spec/
38
+ ├── spec_helper.rb
39
+ ├── locales_spec.rb
40
+ └── nodes/
41
+ └── root_spec.rb
42
+ ```
43
+
44
+ Then:
45
+
46
+ ```
47
+ cd tictactoe
48
+ bundle install
49
+ bundle exec rspec # passes
50
+ bundle exec rubocop # green
51
+ ruby main.rb # a window saying "Hello from tictactoe!"
52
+ ```
53
+
54
+ The generator builds the class name from the project name. It splits on
55
+ underscores and dashes and capitalises each part. `tic_tac_toe` and
56
+ `tic-tac-toe` both give `TicTacToeGame`.
57
+
58
+ The project records two versions, both taken from the running generator rather
59
+ than from a template. The `Gemfile` pins the engine loosely
60
+ (`gem 'rgame', '~> 0.3'` from rgame 0.3.0). `.ruby-version` records the exact Ruby that ran
61
+ `rgame new`, the one interpreter the project is known to work on. The Gemfile
62
+ reads that file instead of repeating the number:
63
+
64
+ ```ruby
65
+ ruby file: '.ruby-version'
66
+ ```
67
+
68
+ Version managers and Bundler both read `.ruby-version`, so the two cannot drift
69
+ apart.
70
+
71
+ **Bundler treats that line as an exact requirement.** On any other Ruby,
72
+ `bundle install` refuses to run. `4.0` does not match `4.0.5`; it matches only
73
+ `4.0`. To accept a range, state it in the `Gemfile`. `.ruby-version` must stay a
74
+ plain version number, because version managers read it:
75
+
76
+ ```ruby
77
+ ruby '~> 4.0' # instead of `ruby file: '.ruby-version'`
78
+ ```
79
+
80
+ ## Why the layout looks like this
81
+
82
+ **The generated tree follows the engine's own layering.** The layout makes the
83
+ right split the easy one in a new project. Three files carry it.
84
+
85
+ **`game.rb` is the only file that requires `rgame/game`,** so it is the only one
86
+ that loads SDL and OpenGL. It is the project's counterpart of
87
+ [`RGame::Game`](game.md): the one class allowed to know both halves of the
88
+ engine.
89
+
90
+ ```ruby
91
+ require 'rgame/game'
92
+ require_relative 'nodes/root'
93
+
94
+ class TictactoeGame < RGame::Game
95
+ WIDTH = 640
96
+ HEIGHT = 480
97
+
98
+ def initialize(**)
99
+ super(root: Root.new,
100
+ caption: 'Tictactoe',
101
+ width: WIDTH,
102
+ height: HEIGHT,
103
+ media_root: File.join(__dir__, 'assets'),
104
+ **)
105
+ end
106
+ end
107
+ ```
108
+
109
+ The bare `**` forwards every keyword to `RGame::Game`, so all its options still
110
+ work. Pass `players: 2` for split-screen. Pass `input:` to drive the game from a
111
+ scripted input backend with no hardware attached.
112
+
113
+ **`nodes/` requires `rgame`,** the graphics-free half. That loads `RGame::Util`
114
+ and `RGame::Engine`, and no graphics library. A node receives a renderer at draw
115
+ time and calls its methods by name. It never stores the renderer and never
116
+ learns its class.
117
+
118
+ ```ruby
119
+ require 'rgame'
120
+
121
+ class Root < RGame::Engine::Node2D
122
+ def initialize
123
+ super
124
+ @greeting = RGame::Engine::Text.new('root.greeting')
125
+ end
126
+
127
+ def on_draw(renderer, _view)
128
+ renderer.text(@greeting, 20, 20)
129
+ end
130
+ end
131
+ ```
132
+
133
+ Override `on_control(actions)`, `on_update(dt)` and `on_draw(renderer, view)`,
134
+ not `control`, `update` or `draw`. The engine does its bookkeeping in the outer
135
+ methods and calls these hooks, so there is no `super` to forget. See
136
+ [Scene graph](scene_graph.md).
137
+
138
+ **`spec/spec_helper.rb` also requires `rgame`,** and `.rspec` loads it before
139
+ every spec. It requires every file under `nodes/` too. The generated suite
140
+ therefore runs headless. It has no window, no GPU and no clock, and `RGame::Core` is undefined.
141
+ A spec that names Core fails loudly instead of opening a window.
142
+
143
+ For the same reason, the generated spec uses a plain spy, not a verified double.
144
+ The renderer it replaces lives on the far side of a line the suite does not
145
+ cross. The generated `.rubocop.yml` turns `RSpec/VerifiedDoubles` off and writes
146
+ down that reason:
147
+
148
+ ```ruby
149
+ RSpec.describe Root do
150
+ describe '#on_draw' do
151
+ it 'draws its greeting from the English table' do
152
+ renderer = spy('renderer')
153
+
154
+ described_class.new.on_draw(renderer, nil)
155
+
156
+ expect(renderer).to have_received(:text).with('Hello from tictactoe!', 20, 20)
157
+ end
158
+ end
159
+ end
160
+ ```
161
+
162
+ The node passes its `Text` to `text`, so the spy records the `Text`. It still
163
+ matches the String, because a `Text` is `==` to the String it reads. See
164
+ [`Text`](toolbox.md#text--the-string-a-node-draws).
165
+
166
+ Put new game logic under `nodes/`, and the whole simulation stays testable in
167
+ milliseconds with no display, however large the game grows. Logic in `game.rb`
168
+ loses that.
169
+
170
+ ## Text comes from a translation table
171
+
172
+ **The generated root node draws a key, not a String.** `assets/locales/en.yml`
173
+ holds the text, in Rails' format:
174
+
175
+ ```yaml
176
+ en:
177
+ root:
178
+ greeting: "Hello from tictactoe!"
179
+ ```
180
+
181
+ `RGame::Game` loads every `.yml` under `assets/locales/` and picks the player's
182
+ language from their operating system. See
183
+ [Game](game.md#translations-and-the-players-language). The node builds an
184
+ [`Engine::Text`](toolbox.md#text--the-string-a-node-draws) once and draws it
185
+ every frame. To add a language, add a file such as `de.yml` with the same keys.
186
+ [Localization](localization.md) covers the format, plurals and the fallback.
187
+
188
+ **The generated spec helper loads the same tables before every example.** It
189
+ reads the files once, then calls `I18n.reset`, loads each table and sets
190
+ `I18n.missing = :raise`. Every example therefore starts in the default locale
191
+ with the game's own tables, whatever the example before it loaded. A spec that
192
+ draws a key no table has fails with `I18n::MissingKey`.
193
+
194
+ The spec helper names `assets/locales` itself, because `spec/` cannot load
195
+ `game.rb`. Move the directory in one place and every spec that draws a key
196
+ fails. Reloading costs about 8 µs per key per example.
197
+
198
+ **`spec/locales_spec.rb` fails while a language lacks a key.** It checks that
199
+ the default locale has a table, and that `I18n.missing_keys` is empty for every
200
+ loaded locale. Its failure names each locale and its missing keys, such as
201
+ `{de: ["root.greeting"]}`. In the game itself, a missing key falls back to the
202
+ default locale's text, and a key no table has shows as itself.
203
+
204
+ ## The generated RuboCop configuration
205
+
206
+ The generator loads `rubocop-performance`, `rubocop-rspec` and rgame's own cops,
207
+ relaxes the `Metrics/*` cops for a game's long `update` and `draw` methods, and
208
+ allows short coordinate names. `RSpec/SpecFilePathFormat` skips
209
+ `spec/locales_spec.rb`, which describes `I18n` but checks the tables rather than a
210
+ source file.
211
+
212
+ **The gem ships its cops as a RuboCop plugin.** The generated `.rubocop.yml`
213
+ loads it by path and class:
214
+
215
+ ```yaml
216
+ plugins:
217
+ - rgame/rubocop:
218
+ plugin_class_name: RuboCop::Game::Plugin
219
+ ```
220
+
221
+ The plain form, `- rgame`, would make RuboCop require the whole engine just to
222
+ lint. An existing project gets the cops by adding those three lines.
223
+
224
+ | Cop | Refuses | Where |
225
+ |---|---|---|
226
+ | `Game/NoInterpolationInHotPath` | string interpolation in a per-frame method | everywhere but `spec/` |
227
+ | `Game/NoNeedlessAllocation` | a throwaway Array or Range literal on a per-frame path | everywhere but `spec/` |
228
+ | `Game/DrawInLocalSpace` | a node's draw method reading its own `x`, `y` or `world_x` | everywhere |
229
+ | `Game/NoLiteralText` | a String literal passed to `text` or `text_width` | everywhere |
230
+ | `Game/NoCoreInEngineLayer` | naming `RGame::Core`, or requiring `rgame/core` or `rgame/game` | `nodes/` and `spec/` |
231
+
232
+ A per-frame method is `update`, `control`, `draw`, `on_update`, `on_control` or
233
+ `on_draw`, or any method with a `# hot-path` comment on the line above its
234
+ `def`. For a label that changes, the answer to the first cop is an
235
+ [`Engine::Text`](toolbox.md#text--the-string-a-node-draws).
236
+
237
+ `Game/NoCoreInEngineLayer` guards the headless line the layout above draws. A
238
+ spec that names `RGame::Core` already fails when it runs, but the cop also
239
+ catches a branch no spec reaches. A bare `Core` counts only inside
240
+ `module RGame`, so a game's own `Core` module passes.
241
+
242
+ `Game/NoEngineInCoreLayer` ships too, switched off. It guards the engine's own
243
+ repository, and a game has no layer for it to guard.
244
+
245
+ ## Adding to the generator
246
+
247
+ `rgame new` derives its file list from `lib/rgame/cli/templates/`. A new file in
248
+ a generated project needs a new template and nothing else; there is no manifest.
249
+ The generator writes only files, so it creates a directory only by writing a
250
+ template into it.
251
+ Templates are ERB and may call `app_name`, `game_class`, `caption`,
252
+ `ruby_version` and `rgame_requirement`.
253
+
254
+ **No template may have a name starting with a dot.** The gemspec packages
255
+ `lib/**/*` with `Dir.glob`, which skips dotfiles. A template called `.gitignore`
256
+ would work in a checkout but be missing from the installed gem. Dotfile
257
+ templates therefore use a plain name (`gitignore.tt`).
258
+ `RGame::CLI::NewProject::DOTFILES` renames them on the way out.
259
+ `spec/packaging_spec.rb` fails if a dotfile template appears.