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
data/docs/api/values.md CHANGED
@@ -1,9 +1,12 @@
1
1
  # Value types
2
2
 
3
- Everything in `RGame::Util` is a *value*: cheap, comparable, owning no window,
4
- GPU handle or file. That is what makes them safe for game logic to hold as
5
- attributes — they load with `require 'rgame'` and pull in no graphics libraries
6
- at all.
3
+ **Everything in `RGame::Util` is a value**: cheap, comparable, and owning no
4
+ window or GPU handle. Game logic can therefore hold these types as attributes.
5
+ They load with `require 'rgame'` and pull in no graphics library.
6
+
7
+ `SaveFile` touches the disk, and still belongs here. It holds a *path*, not an
8
+ open handle. It opens a file, reads or writes it, and closes it again. What
9
+ decides the namespace is ownership, not I/O.
7
10
 
8
11
  ```ruby
9
12
  require 'rgame'
@@ -11,8 +14,8 @@ require 'rgame'
11
14
 
12
15
  ## `RGame::Util::Color`
13
16
 
14
- An RGBA colour. Instances are **frozen** and compare **by value**, so one can be
15
- shared freely and used as a Hash key.
17
+ `Color` is an RGBA colour. Instances are **frozen** and compare **by value**, so
18
+ you can share one freely and use it as a Hash key.
16
19
 
17
20
  ```ruby
18
21
  Color = RGame::Util::Color
@@ -30,7 +33,18 @@ Color.from_packed(0xFF8000C8) # 0xRRGGBBAA
30
33
  | `==`, `eql?`, `hash` | Value semantics. |
31
34
  | `inspect` | `#<RGame::Util::Color r=1 g=2 b=3 a=4>` |
32
35
 
33
- Named colours: `Color::WHITE`, `Color::BLACK`, `Color::TRANSPARENT`.
36
+ ### The named palette
37
+
38
+ ```ruby
39
+ Color::WHITE Color::BLACK Color::TRANSPARENT
40
+ Color::RED Color::GREEN Color::BLUE
41
+ Color::YELLOW Color::CYAN Color::MAGENTA
42
+ Color::ORANGE Color::PURPLE Color::BROWN Color::PINK
43
+ Color::GRAY Color::LIGHT_GRAY Color::DARK_GRAY
44
+ ```
45
+
46
+ Every named colour except `TRANSPARENT` is opaque. They use the CSS/X11 values,
47
+ so `Color::ORANGE` matches the orange a colour picker gives you.
34
48
 
35
49
  ### Out-of-range components raise
36
50
 
@@ -38,21 +52,22 @@ Named colours: `Color::WHITE`, `Color::BLACK`, `Color::TRANSPARENT`.
38
52
  Color.new(300, 0, 0) # ArgumentError: red must be in 0..255, got 300
39
53
  ```
40
54
 
41
- Silently clamping would hide the bug that produced the 300.
55
+ Clamping would hide the bug that produced the 300.
42
56
 
43
57
  ### `Color.coerce`
44
58
 
45
- Drawing calls accept a colour in several forms, and `coerce` is the single
46
- place that conversion happens:
59
+ Drawing calls accept a colour in several forms. **`coerce` performs every such
60
+ conversion:**
47
61
 
48
62
  ```ruby
49
63
  Color.coerce(nil) # => Color::WHITE — an untinted draw
50
64
  Color.coerce([255, 128, 0]) # => opaque
51
65
  Color.coerce([255, 128, 0, 64]) # => with alpha
52
- Color.coerce(Color::WHITE) # => returned unchanged, not copied
66
+ Color.coerce(Color::WHITE) # => Color::WHITE — the same object, not copied
53
67
  ```
54
68
 
55
- Anything else raises `TypeError`; a wrongly-sized array raises `ArgumentError`.
69
+ Any other type raises `TypeError`. An array of the wrong size raises
70
+ `ArgumentError`.
56
71
 
57
72
  ### Value semantics in practice
58
73
 
@@ -65,14 +80,14 @@ a == b # => true — two objects, one value
65
80
  a.frozen? # => true
66
81
  ```
67
82
 
68
- Because a colour is frozen, handing the same one to two sprites is safe: nobody
69
- can tint it out from under the other.
83
+ A frozen colour is safe to hand to two sprites. Neither can change it under the
84
+ other.
70
85
 
71
86
  ## `RGame::Util::Tensor`
72
87
 
73
- A fixed-size three-dimensional grid, addressed as `[x, y, z]`. Backed by a
74
- single flat array in C, so it stays compact for the sizes a tile map or a
75
- lighting volume needs.
88
+ `Tensor` is a fixed-size three-dimensional grid, addressed as `[x, y, z]`. One
89
+ flat C array backs it, so it stays compact at the sizes a tile map or a lighting
90
+ volume needs.
76
91
 
77
92
  ```ruby
78
93
  grid = RGame::Util::Tensor.new(width, height, depth)
@@ -86,8 +101,8 @@ grid.width # also #height and #depth
86
101
 
87
102
  Cells hold any Ruby object. `initial:` is optional and defaults to `nil`.
88
103
 
89
- The layout is x-fastest, then y, then z, so one z-slice is a contiguous run
90
- worth knowing if you iterate a layer at a time and care about locality.
104
+ **x varies fastest, then y, then z**, so each z-slice is one contiguous run. For
105
+ good locality, iterate a layer at a time:
91
106
 
92
107
  ```ruby
93
108
  grid.depth.times do |z|
@@ -100,26 +115,123 @@ grid.depth.times do |z|
100
115
  end
101
116
  ```
102
117
 
118
+ ## `RGame::Util::SolidGrid`
119
+
120
+ `SolidGrid` records which cells of a tile grid are solid, one byte per cell, in C.
121
+ **It is the single store of a tile world's solidity.**
122
+ [`TileWorld`](components.md#tileworld) builds one from its map. The blockers that
123
+ stop a walker and the search that plans a route both read it, so they never
124
+ disagree about a wall.
125
+
126
+ ```ruby
127
+ fence = RGame::Util::SolidGrid.build(8, 3) { |col, row| col == 3 && row < 2 } # asks once per cell, row by row
128
+ fence.solid?(3, 1) # => true
129
+ fence.solid?(-1, 0) # => false — outside the grid is open
130
+ fence.width # => 8 — and #height
131
+
132
+ field = RGame::Util::SolidGrid.new(8, 3) # every cell open
133
+ field.set_solid(3, 1, true)
134
+ field.set_solid(3, 1, true)
135
+ field.revision # => 1 — the second write changed nothing
136
+ ```
137
+
138
+ - **`revision` counts changes.** It advances only when `set_solid` changes a
139
+ cell. Anything derived from the cells can then tell whether it is stale, such
140
+ as a [`RouteSearch`](#rgameutilroutesearch)'s region labels.
141
+ - **Coordinates are Integers**; anything else raises `TypeError`. An Integer
142
+ outside the grid, however large, is outside. `solid?` answers `false` there.
143
+ `set_solid` raises `IndexError`, because a wall written nowhere does not exist.
144
+ - **The size is fixed.** A negative size, or more than `2**31 - 1` cells, raises
145
+ `ArgumentError`. A zero size gives an empty grid. A grid cannot be `dup`ed,
146
+ because a search holds on to the grid it was built over.
147
+ - **`SolidGrid.debug_live_grids`** returns how many grids hold cells. rgame's own
148
+ suite uses it to check that a grid frees its cells.
149
+
150
+ ## `RGame::Util::RouteSearch`
151
+
152
+ `RouteSearch` finds connected regions and A* routes over a `SolidGrid`, in C.
153
+ Games use [`Engine::NavGrid`](toolbox.md#navgrid--routes-over-a-tile-grid), which
154
+ wraps a `RouteSearch` and states the route rules.
155
+
156
+ ```ruby
157
+ grid = RGame::Util::SolidGrid.build(8, 3) { |col, row| col == 3 && row < 2 }
158
+ search = RGame::Util::RouteSearch.new(grid)
159
+
160
+ search.find(0, 0, 7, 0) # => [[0, 0], [1, 1], [2, 2], [3, 2], [4, 2], [5, 1], [6, 0], [7, 0]]
161
+ search.region(0, 0) # => 0 — nil for a solid cell or one outside the grid
162
+ search.grid # => the grid — which the search keeps alive
163
+ ```
164
+
165
+ - **It reads its grid and never writes it.** Any number of searches may share a
166
+ grid. Each sees a change on its next query. It recomputes region labels then,
167
+ but only if the grid's `revision` moved.
168
+ - **A repeated query allocates nothing but its result.** The search allocates
169
+ its per-cell buffers once. Its heap grows to the largest query so far and
170
+ stays there. One search is therefore not safe to use from two threads at once.
171
+ - Coordinates follow `SolidGrid`'s rules. A non-Integer raises `TypeError`, and
172
+ for a cell outside the grid `find` and `region` return `nil`.
173
+ - **`RouteSearch.debug_live_searches`** returns how many searches hold buffers,
174
+ for leak checks in rgame's own suite.
175
+
176
+ ## `RGame::Util::TileSweep`
177
+
178
+ `TileSweep` tests an axis-aligned box against the solid tiles of a `SolidGrid`,
179
+ at a given tile size, in C. Games use
180
+ [`Engine::TileBlockers`](internals.md#tileblockers--the-tile-grid-as-a-blocker-source).
181
+ Every mover that declares `blocked_by: [:tiles]` resolves against it.
182
+ `TileSweep` does the arithmetic underneath, and both of its queries share one
183
+ implementation.
184
+
185
+ ```ruby
186
+ grid = RGame::Util::SolidGrid.build(20, 15) { |col, _row| col == 5 } # a wall at x 80..96
187
+ sweep = RGame::Util::TileSweep.new(grid, 16, 16)
188
+
189
+ sweep.resolve_x(58.0, 32.0, 12, 6, 14.0) # => 68.0 — flush against the wall
190
+ sweep.resolve_y(58.0, 32.0, 12, 6, 4.0) # => 36.0
191
+ sweep.travel?(10.0, 32.0, 12, 6, 50.0, 0) # => true
192
+ sweep.travel?(10.0, 32.0, 12, 6, 90.0, 0) # => false
193
+ sweep.grid # => the grid — which the sweep keeps alive
194
+ ```
195
+
196
+ - **A box is a top-left corner and a size, in pixels.** Results are Floats.
197
+ Outside the grid is open.
198
+ - **`resolve_x` and `resolve_y`** move the box along one axis. If the box would
199
+ enter a solid tile, they snap it flush against that tile. They assume a step
200
+ smaller than a tile.
201
+ - **`travel?`** answers whether the box can move `(dx, dy)` without any resolve
202
+ stopping it short. It sweeps overlapping half-tile windows a quarter tile
203
+ apart, resolving each in both axis orders. The answer holds for a walker that
204
+ steps less than a quarter tile at a time.
205
+ - **It reads its grid and never writes it**, so the next call sees a
206
+ `set_solid`. No query allocates.
207
+ - **Refusals.** A tile size that is not positive and finite raises
208
+ `ArgumentError`. So does a travel too long to sweep, beyond `2**31 - 1`
209
+ windows. A coordinate that is not a number raises `TypeError`. A non-finite
210
+ coordinate raises `FloatDomainError`, except in a resolve that does not move:
211
+ that returns the box where it is.
212
+ - **`TileSweep.debug_live_sweeps`** returns how many sweeps are allocated, for
213
+ leak checks in rgame's own suite.
214
+
103
215
  ## `RGame::Util::Z`
104
216
 
105
- The vocabulary of draw order: which band a thing is drawn in, and the arithmetic
106
- that turns a band plus a position in the tree into the single number the renderer
107
- sorts a frame by.
217
+ `Z` is the vocabulary of draw order. It names the bands, and turns a band plus a
218
+ position in the tree into the single number the renderer sorts by.
108
219
 
109
220
  ```ruby
110
221
  RGame::Util::Z::BANDS # => [:world, :hud, :overlay, :debug]
111
222
  RGame::Util::Z::DEFAULT # => :world
112
- RGame::Util::Z::Z_MIN # => -512, the smallest `z:` a drawing call may pass
223
+ RGame::Util::Z::Z_MIN # => -512 the smallest `z:` a drawing call may pass
113
224
  RGame::Util::Z::Z_MAX # => 511
225
+ RGame::Util::Z.band?(:hud) # => true
114
226
  ```
115
227
 
116
- It lives here for the same reason [`Controls`](input.md) does: both the scene
117
- graph (which decides a node's band) and the renderer (which turns one into a z)
118
- have to name it, and neither may name the other's layer.
228
+ It lives in `Util` for the same reason as [`Controls`](input.md). The scene
229
+ graph decides a node's band and the renderer turns it into a z. Both must name
230
+ `Z`, and neither may name the other's layer.
119
231
 
120
- Games rarely touch it. What a game writes is a node's `z` and, occasionally, a
121
- `band:` see [the scene graph](scene_graph.md#draw-order). What it buys is that
122
- `z` numbers cannot leak between nodes and bands cannot leak into each other:
232
+ **Games rarely touch `Z`.** A game sets a node's `z` and, occasionally, a
233
+ `band:`; see [the scene graph](scene_graph.md#draw-order). `Z` guarantees that `z`
234
+ numbers cannot leak between nodes, and bands cannot leak into each other:
123
235
 
124
236
  | | |
125
237
  |---|---|
@@ -127,7 +239,78 @@ Games rarely touch it. What a game writes is a node's `z` and, occasionally, a
127
239
  | `Z_MIN`…`Z_MAX` | what a `z:` on a drawing call may be; anything else raises |
128
240
  | `STRIDE` | `2**40` — the gap between bands, which no `z:` can cross |
129
241
 
130
- Every value is an integer below `2**42`, and the `double` the draw queue sorts on
131
- is exact below `2**53`, so two different slots can never compare equal by
132
- rounding which would show up as two sprites swapping places between frames, and
133
- would be very hard to recognise as a precision problem.
242
+ Every value is an integer below `2**42`. The draw queue sorts on a `double`, which
243
+ is exact below `2**53`. Two different slots therefore never round to the same
244
+ key. If they did, two sprites would swap places between frames, and nobody would
245
+ suspect a precision problem.
246
+
247
+ ## `RGame::Util::SaveFile`
248
+
249
+ `SaveFile` stores a game's saved state as one JSON file.
250
+
251
+ ```ruby
252
+ save = RGame::Util::SaveFile.new('slot1.json', game: 'sheepdog')
253
+
254
+ save.write(dog: [120, 80], sheep: [[40, 40], [90, 30]])
255
+ save.read # => { dog: [120, 80], sheep: [[40, 40], [90, 30]] }
256
+ save.exist? # => true
257
+ save.delete
258
+ ```
259
+
260
+ `SaveFile.new(name, game: 'rgame', dir: nil)` places `name` in
261
+ `SaveFile.directory(game)`, or in `dir:` when given. `path` returns the full path.
262
+ `delete` does nothing when the file is already gone.
263
+
264
+ Keys come back as Symbols, so a game writes and reads one shape. JSON comes from
265
+ the standard library, so `SaveFile` adds no runtime dependency.
266
+
267
+ ### Reading never raises
268
+
269
+ **`read` returns its default for any file it cannot use.** The default is `{}`,
270
+ or whatever you pass. That covers a file that is missing, empty, truncated, not
271
+ JSON, or JSON that is not an object. A directory in the way and an unreadable
272
+ file get the same answer.
273
+
274
+ This is why to prefer `SaveFile` over `JSON.parse(File.read(path))`. A save file
275
+ is the one input a game did not produce this run. It survives crashes, full
276
+ disks, killed processes, text editors, and copies from other machines. A game
277
+ that raises on any of those cannot start again. The player's only remedy is to
278
+ find and delete a file nobody told them about. Losing a save is bad; refusing to
279
+ launch is worse.
280
+
281
+ **Writing still raises.** A failed read has a sensible answer: "there is no
282
+ save". A failed write has none. Discarding progress without a word gets noticed
283
+ hours later, when the progress is gone.
284
+
285
+ ### Writing is atomic
286
+
287
+ **`write` never leaves half a save.** It writes a temporary file beside the
288
+ target and renames it over the top. `File.rename` within one directory is atomic
289
+ on every platform the engine supports. A save is either the old one or the new
290
+ one.
291
+
292
+ A game most wants to save on the way out, which is also when it is most likely
293
+ to be killed. A plain `File.write` truncates first and fills in after. A crash in
294
+ between leaves a zero-byte file where the afternoon's progress was.
295
+
296
+ ### Where saves go
297
+
298
+ `SaveFile.directory(game)` follows each platform's convention instead of
299
+ dropping a dotfile in the home directory:
300
+
301
+ | | |
302
+ |---|---|
303
+ | Linux | `$XDG_DATA_HOME/<game>`, or `~/.local/share/<game>` |
304
+ | macOS | `~/Library/Application Support/<game>` |
305
+ | Windows | `%APPDATA%\<game>` |
306
+
307
+ Pass `dir:` to store the file somewhere else. Specs do this.
308
+
309
+ ### What it does not do
310
+
311
+ **`SaveFile` does not serialize a scene tree, and a game should not either.** A
312
+ scene is a recipe; a save file is state. The scene rebuilds itself identically
313
+ every run, and the save supplies the few facts that differ. `examples/save_load`
314
+ shows the pattern, matching objects back up without naming any node.
315
+ `examples/save_load_ids` shows the case that needs names, using
316
+ [`Components::Identity`](components.md#identity).
@@ -0,0 +1,322 @@
1
+ # Example assets
2
+
3
+ Everything the examples draw and play. Fifteen files besides this one, about
4
+ 124 KB in total — of which the music is 93 KB, and the reason for `tools/shrink_ogg.c`.
5
+
6
+ ## Why these files and not the ones in `media/`
7
+
8
+ `media/` is gitignored because its contents cannot be redistributed. These can:
9
+ **this directory ships inside the gem**, so every `gem install rgame`
10
+ redistributes each file below to somebody else. That is the test any asset here
11
+ has to pass — not "may I use it in my game" but *"may I hand copies of it to
12
+ everyone who installs this library"* — and it is why the rule is CC0 or
13
+ authored here, with nothing in between. A licence that merely permits use, or
14
+ asks for a credit line, would attach an obligation to rgame and to everyone
15
+ downstream of it.
16
+
17
+ CC0 waives copyright entirely, so none of the sources below require a licence
18
+ file to be shipped alongside them or a credit to be given. This README is
19
+ recorded anyway: provenance is the thing that becomes impossible to reconstruct
20
+ later, and "where did this PNG come from" is not a question anyone should have
21
+ to answer from memory.
22
+
23
+ ## The files
24
+
25
+ ### `tileset.png` — Kenney, *Tiny Town*
26
+
27
+ - Source: <https://kenney.nl/assets/tiny-town>
28
+ - Licence: CC0 1.0 (stated on the page and in the `License.txt` inside the
29
+ download)
30
+ - Modification: none. This is `Tilemap/tilemap_packed.png` from the pack,
31
+ copied unchanged.
32
+
33
+ 16x16 tiles, 12 columns x 11 rows = 132 tiles, 192x176 pixels. Kenney asks for
34
+ a credit but does not require one; the licence is CC0 either way.
35
+
36
+ ### `tileset.tsx` — ours
37
+
38
+ A Tiled tileset over `tileset.png`. Written here rather than taken from the
39
+ pack, because the pack has no Tiled metadata and because **collision lives in
40
+ this file**: `RGame::Engine::Tileset` treats a tile as solid when it carries an
41
+ `<objectgroup>` with at least one object, which is what Tiled's per-tile
42
+ collision editor writes. There is no solid-tile list in code to fall back on, so
43
+ a tile with no shape here is walkable no matter what it looks like.
44
+
45
+ Solid tiles are the trees and the fence pieces: **3, 4, 5, 15, 16, 27, 28** and
46
+ **44, 45, 46, 47**. Everything else — grass, sand, mushrooms, plants, buildings,
47
+ items — is walkable. That is a deliberate floor rather than an inventory: the
48
+ examples only need vegetation and fences to be obstacles, and marking a tile
49
+ solid that no example places is a claim nothing checks. Add shapes in Tiled as
50
+ new examples need them.
51
+
52
+ ### `town.tmx` — ours
53
+
54
+ 60x40 tiles = 960x640 pixels, deliberately larger than the 640x480 window on
55
+ both axes so a camera has somewhere to scroll. Two layers, `ground` (entirely
56
+ walkable) and `obstacles` (a tree border, a fence across the middle, scattered
57
+ trees).
58
+
59
+ Layer data is **base64 + zlib**, which is what `RGame::Engine::TileMap.parse`
60
+ reads — it inflates the layer and unpacks little-endian `uint32` gids. CSV will
61
+ not load. Tiled writes this format when the layer format is set to "Base64
62
+ (zlib compressed)"; keep it that way when editing.
63
+
64
+ The fence has exactly one gap, at x=12..14, far west of both clearings. The
65
+ placement is the point, and it took two tries to get right:
66
+
67
+ - **A gap between the start and the goal is not an obstacle.** With the gap at
68
+ x=29..31 the shortest route cost exactly the straight-line distance — 55 steps
69
+ against a 55-step Manhattan distance — so a pathfinder would have drawn what
70
+ looks like a straight line. Out west, the same trip is 84 steps against 22,
71
+ and the walker has to head *away* from its goal to get through.
72
+ - **A fence has to span the whole interior.** Stopping it a tile short of the
73
+ border left a second gap nobody planned, at x=58, and the route quietly used
74
+ that one instead. It now runs x=1..58, and the fence row is solid everywhere
75
+ except the three gap tiles.
76
+
77
+ ### `ui.png` + `ui.json` — Kenney, *UI Pack - Pixel Adventure*
78
+
79
+ - Source: <https://kenney.nl/assets/ui-pack-pixel-adventure>
80
+ - Licence: CC0 1.0 (stated on the page and in the `License.txt` inside the
81
+ download)
82
+ - Modification: five 32x32 tiles were cut out of
83
+ `Tilesheets/Large tiles/Thick outline/tilemap_packed.png` and laid side by
84
+ side. The pixels are untouched; only the sheet is ours.
85
+
86
+ The pack is 91 tiles and we use five, so shipping the strip rather than the
87
+ sheet keeps this to 1 KB and makes the descriptor readable — each element is at
88
+ a round multiple of 32.
89
+
90
+ **This is not optional chrome.** `RGame::Engine::UI::PanelButton` draws its
91
+ background with `renderer.nine_slice`, and a nine-slice id is resolved by
92
+ *registration* only — it names an element of an atlas, never a file — so
93
+ `UI::Menu` cannot draw at all without one of these registered. Which elements
94
+ are needed is not our choice either: `PanelButton::STYLE` names `button_idle`,
95
+ `button_focus`, `button_pressed` and `button_disabled`, and a menu draws each of
96
+ them when an item reaches that state.
97
+
98
+ Two things constrained which tiles could be used, both discovered by looking:
99
+
100
+ - **Several of the pack's panels are frames with transparent middles.** They
101
+ read as solid panels on the sheet's dark background and then show the world
102
+ through them. The five here are all filled.
103
+ - **`PanelButton`'s label colour defaults to** a dark brown, with a muted grey
104
+ for a disabled item, so with the default the buttons have to be *light* or the
105
+ label disappears into them. `label_color:` changes it.
106
+
107
+ `border` is 8 for every element: the largest decorated frame in the set is 8
108
+ pixels, the interiors are flat, and a uniform value keeps a button's inner
109
+ geometry from shifting as it changes state.
110
+
111
+ ### `hero.png` + `hero.json` — sodri's *Character 4 directional walking*, repacked
112
+
113
+ - Source: <https://opengameart.org/content/character-4-directional-walking>
114
+ - Author: sodri
115
+ - Licence: CC0
116
+ - Modifications, both ours: the white background was keyed out, and the four
117
+ separate strips were repacked into one uniform sheet.
118
+
119
+ The original is four PNGs — `walk_down`/`walk_up` at 14x22 per frame,
120
+ `walk_left`/`walk_right` at 13x22 — fully opaque, with a pure white background.
121
+ Two things had to change before `RGame::Core::SpriteSheet` could read it:
122
+
123
+ - **Transparency.** Pure white was replaced with alpha 0. Checked first rather
124
+ than assumed: flood-filling from the borders showed white is only ever
125
+ background — the handful of enclosed white pixels are the gaps between arm and
126
+ torso — and the art uses 7 to 9 colours with no near-white among them, so a
127
+ flat colour key cannot eat anything intended.
128
+ - **A uniform grid.** A sheet is one fixed cell size, so each frame was centred
129
+ horizontally in a 16x22 cell (`(16 - w) // 2`, so the 13px-wide side frames
130
+ sit one pixel left of true centre — half a pixel of asymmetry between facings,
131
+ which nothing can see).
132
+
133
+ `walk_left` is not in the sheet. Every left frame is a pixel-exact mirror of its
134
+ right counterpart — verified frame by frame, not assumed — so the descriptor
135
+ reuses row 2 with `"flip_x": true` and the sheet is three rows instead of four.
136
+
137
+ Layout, 6 columns x 3 rows of 16x22:
138
+
139
+ | Row | Animation |
140
+ |---|---|
141
+ | 0 | `walk_down`, and `stand` is its first column |
142
+ | 1 | `walk_up` |
143
+ | 2 | `walk_right`, and `walk_left` mirrored |
144
+
145
+ There is no idle art in the original, so `stand` is a single frame off the walk
146
+ cycle rather than an animation of its own.
147
+
148
+ ### `glyphs.png` + `glyphs.json` — Kenney, *Input Prompts*, repacked
149
+
150
+ - Source: <https://kenney.nl/assets/input-prompts>
151
+ - Licence: CC0 1.0 (stated on the page and in the `License.txt` inside the
152
+ download)
153
+ - Modification: five 64x64 PNGs were laid side by side into one strip. The
154
+ pixels are untouched; only the sheet and the descriptor are ours.
155
+
156
+ The pack is thousands of files across seventeen controller families, and
157
+ `examples/input_glyphs` names five buttons, so the strip is 3 KB where the
158
+ download is 5 MB.
159
+
160
+ Layout, 5 columns x 1 row of 64x64, and **the order is the example's table**:
161
+ `GLYPH_COLUMN` in `examples/input_glyphs/main.rb` maps a `Controls` button id to
162
+ a column here.
163
+
164
+ | Column | Button id | From the pack |
165
+ |---|---|---|
166
+ | 0 | `KEY_SPACE` | `Keyboard & Mouse/Default/keyboard_space.png` |
167
+ | 1 | `KEY_RETURN` | `Keyboard & Mouse/Default/keyboard_enter.png` |
168
+ | 2 | `KEY_ESCAPE` | `Keyboard & Mouse/Default/keyboard_escape.png` |
169
+ | 3 | `PAD_A` | `Xbox Series/Default/xbox_button_color_a.png` |
170
+ | 4 | `PAD_B` | `Xbox Series/Default/xbox_button_color_b.png` |
171
+
172
+ **Xbox rather than PlayStation or Switch**, because SDL's button names are
173
+ Xbox's — `PAD_A` is SDL's A — and a sheet whose faces disagree with the ids
174
+ would make every prompt a translation. A game that wants the pad in the player's
175
+ actual hands ships a second sheet per family and picks a table; the ids do not
176
+ change, only the pictures.
177
+
178
+ The keyboard glyphs are light and the pad glyphs are the pack's colour versions,
179
+ so both need a dark panel under them — which is what `examples/input_glyphs`
180
+ draws.
181
+
182
+ ### `icons.png` + `icons.json` — Kenney, *Game Icons*, repacked
183
+
184
+ - Source: <https://kenney.nl/assets/game-icons>, mirrored with the same licence
185
+ at <https://opengameart.org/content/game-icons>
186
+ - Licence: CC0 1.0 (stated on the page and in the `license.txt` inside the
187
+ download)
188
+ - Modification: eight 50x50 PNGs were laid side by side into one strip. The
189
+ pixels are untouched; only the sheet and the descriptor are ours.
190
+
191
+ Layout, 8 columns x 1 row of 50x50, named in the descriptor's `images` section
192
+ in `snake_case`:
193
+
194
+ | Column | Name | From the pack |
195
+ |---|---|---|
196
+ | 0 | `home` | `PNG/White/1x/home.png` |
197
+ | 1 | `gear` | `PNG/White/1x/gear.png` |
198
+ | 2 | `save` | `PNG/White/1x/save.png` |
199
+ | 3 | `star` | `PNG/White/1x/star.png` |
200
+ | 4 | `trophy` | `PNG/White/1x/trophy.png` |
201
+ | 5 | `audio_on` | `PNG/White/1x/audioOn.png` |
202
+ | 6 | `music_on` | `PNG/White/1x/musicOn.png` |
203
+ | 7 | `locked` | `PNG/White/1x/locked.png` |
204
+
205
+ **White, because a tint is a multiply.** The white variant is RGB 255 with the
206
+ shape in alpha only, so `renderer.image(..., color:)` — and `UI::IconButton`'s
207
+ per-state tints — colour it exactly; the black variant would stay black under
208
+ any tint.
209
+
210
+ **1x, because images sample nearest-neighbour.** A 50-pixel icon in
211
+ `examples/radial_menu`'s 64-pixel slots draws at scale 1 and the chosen one in
212
+ the middle at scale 2, both whole numbers; a 2x icon would need scale 0.5 or
213
+ thereabouts, which drops pixel rows unevenly.
214
+
215
+ **A strip, because the separate files carry metadata.** Each PNG in the pack
216
+ holds Adobe XMP, so the eight files are 121 KB; the strip is 2 KB. It is a UI
217
+ atlas rather than a sprite sheet so the icons are cut and registered by name —
218
+ `renderer.register_ui_atlas(game.assets.ui_atlas('icons.json'))` makes
219
+ `UI::IconButton.new(image: :home)` draw.
220
+
221
+ ### `skills.png` + `skills.json` — Kenney, *Cursor Pack*, repacked
222
+
223
+ - Source: <https://kenney.nl/assets/cursor-pack>, version 1.1
224
+ - Licence: CC0 1.0 (the `License.txt` inside the download reads "License:
225
+ (Creative Commons Zero, CC0)")
226
+ - Modification: five 64x64 PNGs were laid side by side into one strip and saved
227
+ as RGBA. The pixels are untouched; only the sheet and the descriptor are ours.
228
+
229
+ Layout, 5 columns x 1 row of 64x64, named in the descriptor's `images` section
230
+ in `snake_case`:
231
+
232
+ | Column | Name | From the pack |
233
+ |---|---|---|
234
+ | 0 | `wand` | `PNG/Basic/Double/tool_wand.png` |
235
+ | 1 | `wrench` | `PNG/Basic/Double/tool_wrench.png` |
236
+ | 2 | `torch` | `PNG/Basic/Double/tool_torch.png` |
237
+ | 3 | `hammer` | `PNG/Basic/Double/tool_hammer.png` |
238
+ | 4 | `watering_can` | `PNG/Basic/Double/tool_watering_can.png` |
239
+
240
+ `torch` is the pack's flashlight, named for the British word. The pack has
241
+ `tool_hoe`, `tool_shovel`, `tool_axe` and `tool_pickaxe` in the same styles, if
242
+ the bar ever wants more.
243
+
244
+ **`Basic`, not `Outline`.** Both were drawn on `examples/skill_bar`'s discs in
245
+ all four states. `Basic` is a light-grey silhouette with no border, so a tint
246
+ colours all of it: grey at rest, white focused, dim disabled, dark on the gold
247
+ pressed disc. `Outline` adds a black border, which a multiply leaves black —
248
+ a disabled tool barely dims, and a pressed one is a dark shape on gold with its
249
+ edges lost.
250
+
251
+ **`Double` (64 pixels), not `Default` (32).** The glyph sits in about half its
252
+ square, so a 32-pixel cursor on an 80-pixel disc is a thumbnail. Either draws at
253
+ scale 1, which nearest-neighbour sampling needs.
254
+
255
+ **One pack per atlas.** These are not in `icons.png` so that each file's
256
+ provenance stays one paragraph; one more `register_ui_atlas` call is the whole
257
+ cost.
258
+
259
+ ### `blip.ogg` — Kenney, *Interface Sounds*
260
+
261
+ - Source: <https://kenney.nl/assets/interface-sounds>
262
+ - Licence: CC0 1.0 (stated on the page and in the `License.txt` inside the
263
+ download)
264
+ - Modification: none. This is `Audio/click_001.ogg` from the pack, renamed.
265
+
266
+ 4.8 KB. The pack has 100 of these, 4 to 29 KB each, so if an example ever wants
267
+ a second sound it costs nothing to take another.
268
+
269
+ ### `music.ogg` — hernandack's *Short Loops Background Music Pack*, re-encoded
270
+
271
+ - Source: <https://opengameart.org/content/short-loops-background-music-pack>
272
+ - Author: hernandack
273
+ - Licence: CC0
274
+ - Modification: the track *Just Saying Tho* downmixed to mono and re-encoded at
275
+ Vorbis quality -0.1, with `tools/shrink_ogg.c`. **The length is unchanged.**
276
+
277
+ 24.05 seconds, 93 KB, down from 388 KB. The original is stereo, 44.1 kHz,
278
+ ~128 kbps — encoded for listening rather than for a library gem — and mono at a
279
+ low quality setting is background music in a teaching example.
280
+
281
+ **Length was deliberately not touched.** A seamless loop is seamless at exactly
282
+ its own length, because the author arranged for the end to lead back into the
283
+ start. Trimming it to save more bytes would put a seam in the middle of the one
284
+ property the file is shipped to demonstrate.
285
+
286
+ #### Why this is the second track here
287
+
288
+ The first one shipped was *A Brand New Wisdom* from the same pack, chosen on a
289
+ seam measurement of 2.6% — and it loops audibly badly, because **it ends with
290
+ 0.79 seconds of silence.** The wrap has no click. It has a *gap*.
291
+
292
+ That is a hole in the measurement, not bad luck. A seam figure asks "does the
293
+ last sample join smoothly onto the first", and silence joins onto silence
294
+ perfectly. Fading out to nothing is the ordinary way to end a piece of music and
295
+ the ordinary way to ruin a loop, and the number said 0.7% the whole time.
296
+
297
+ `tools/shrink_ogg.c` now reports both, and the whole pack looks different under
298
+ the second one:
299
+
300
+ | track | seam | tail silence | |
301
+ |---|---|---|---|
302
+ | A Brand New Wisdom | 2.6% | **0.79s** | shipped first, wrong |
303
+ | Swinging Sweet | 4.5% | **1.48s** | worse |
304
+ | Winter Dust | 34.9% | 0.00s | no gap, bad seam |
305
+ | **Just Saying Tho** | 2.8% | **0.00s** | shipped now |
306
+ | 8BitBattleLoop (other pack) | 0.0% | 0.01s | cleanest, but 108 KB and chiptune |
307
+
308
+ Measured back off the shipped file: seam 2.3%, no silence at either end.
309
+
310
+ **A loop wants both numbers.** A small seam alone is not evidence.
311
+
312
+ ## Adding an asset here
313
+
314
+ 1. CC0, or drawn in this repo. If the licence says anything about redistribution
315
+ at all, it does not go here — see the top of this file.
316
+ 2. Record it above: source URL, author, licence, and every modification made.
317
+ 3. **Audio is Ogg Vorbis or WAV, and nothing else.** MP3 and FLAC are compiled
318
+ out of miniaudio (`ext/rgame_core/vendor/miniaudio_impl.c`), so a file that
319
+ plays in every desktop player can still fail to load here.
320
+ 4. No leading dots in filenames. `Dir.glob` does not match them, so the gemspec
321
+ would silently leave the file out of the gem while the checkout kept working
322
+ — `spec/packaging_spec.rb` has an example that catches it.
Binary file
@@ -0,0 +1,5 @@
1
+ {
2
+ "image": "glyphs.png",
3
+ "frame_width": 64,
4
+ "frame_height": 64
5
+ }
Binary file
@@ -0,0 +1,12 @@
1
+ {
2
+ "image": "hero.png",
3
+ "frame_width": 16,
4
+ "frame_height": 22,
5
+ "animations": {
6
+ "stand": { "row": 0, "col": 0, "frames": 1, "fps": 1 },
7
+ "walk_down": { "row": 0, "frames": 6, "fps": 8 },
8
+ "walk_up": { "row": 1, "frames": 6, "fps": 8 },
9
+ "walk_right": { "row": 2, "frames": 6, "fps": 8 },
10
+ "walk_left": { "row": 2, "frames": 6, "fps": 8, "flip_x": true }
11
+ }
12
+ }
Binary file