rgame 0.2.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 (253) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +135 -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 +2 -3
  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 +152 -11
  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
@@ -0,0 +1,322 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Save and load, part two — when the save has to name things.
4
+ #
5
+ # Run it:
6
+ #
7
+ # ruby examples/save_load_ids/main.rb
8
+ #
9
+ # Arrows walk the dog. **Tab** points it at the next sheep, **Space** shears the
10
+ # one it is pointing at, **N** brings in a new one. **F5** saves, **F9** loads,
11
+ # **Delete** discards. Every sheep is labelled with its id, and the line from the
12
+ # dog shows which one it is watching. It exercises:
13
+ # - Components::Identity — a stable name for a node;
14
+ # - Util::SaveFile — the same one `examples/save_load` uses;
15
+ # - a save that holds records rather than a bare list of positions.
16
+ #
17
+ # ## Read `examples/save_load` first
18
+ #
19
+ # That one restores a dog by the variable holding it and a flock by array order,
20
+ # and both are correct for what they are. Nothing there needs an id, and adding
21
+ # one would have been ceremony.
22
+ #
23
+ # This is the case it does not cover.
24
+ #
25
+ # ## A collection is not what forces ids — a reference is
26
+ #
27
+ # It is tempting to say "the flock changes, so it needs ids", and that is not
28
+ # quite true. A collection can always be saved as *records* — each sheep writing
29
+ # its own position and wool — and rebuilt from them. Order stops mattering
30
+ # because nothing is being matched up: every sheep in the file becomes a sheep in
31
+ # the world, and that works whether three died or thirty arrived.
32
+ #
33
+ # What cannot be written that way is **the line from the dog to one particular
34
+ # sheep**. A node reference is a pointer into this run of the program; the next
35
+ # run has different objects at different addresses. The only thing that survives
36
+ # the gap is a name both runs agree on, and that is what `Identity` is:
37
+ #
38
+ # target: Identity.of(@target) # an id, or nil
39
+ #
40
+ # On load the flock is rebuilt from records, and then the dog is re-linked by
41
+ # looking for that id among the new sheep. That is the shape `Identity.of` is
42
+ # built for: it takes a node and answers an id, because what a game holds is
43
+ # always the node and what a file can hold is only ever the name.
44
+ #
45
+ # ## Why this is not `Components::Targeting`
46
+ #
47
+ # The engine already has a component that holds a target node, and it is the
48
+ # wrong tool here — which is worth saying, because "there is a component for
49
+ # that" is the first thing a reader will think.
50
+ #
51
+ # `Targeting` *derives* its target: every update it asks the scene's
52
+ # `CollisionWorld` for the nearest collider in range. So it holds a node without
53
+ # ever needing to write one down — a save that omitted it entirely would arrive
54
+ # at the same answer on the next frame. The dog here is the opposite: **Tab**
55
+ # chose this sheep, nothing can recompute that choice, and losing it is the thing
56
+ # a player would notice.
57
+ #
58
+ # Targeting proximity picks the target, every frame — derived
59
+ # this example the player picks it, and it has to survive quitting — chosen
60
+ #
61
+ # **Derived state is never saved; chosen state always is.** Sorting a game's
62
+ # facts into those two piles is most of designing a save file, and the question
63
+ # is cheaper than it looks: could the next frame work this out again? The flock's
64
+ # positions are chosen (the sheep wandered there), the wool is chosen (it grew),
65
+ # the tether is derived from the target — which is why the save holds a target
66
+ # and not a line.
67
+ #
68
+ # ## The allocator is part of the save, and forgetting it is the classic bug
69
+ #
70
+ # `@next_id` is saved and restored with everything else. It has to be: a counter
71
+ # that restarts at 1 on load hands out ids that the restored sheep are already
72
+ # using, and nothing complains at the time. What you see is a dog that follows
73
+ # the wrong animal, hours later, once a duplicate finally gets picked as a
74
+ # target. One number, and the whole scheme rests on it.
75
+ #
76
+ # Try it: save with a few sheep, quit, run again, press **N**. The new sheep gets
77
+ # the next number rather than a duplicate.
78
+ #
79
+ # ## An id that is no longer there
80
+ #
81
+ # `restore_target` falls back to any sheep when the saved id matches none of
82
+ # them. That is not defensive habit — a save file is the one input a game did not
83
+ # produce this run, and it can be hand-edited, copied from another machine, or
84
+ # written by an older version of the game with a different flock. Pointing at
85
+ # nothing is a legitimate state to arrive in, and picking a new target is a
86
+ # better answer than refusing to load.
87
+
88
+ $LOAD_PATH.unshift File.expand_path('../../lib', __dir__)
89
+ require 'rgame/game'
90
+
91
+ Controls = RGame::Util::Controls
92
+ Identity = RGame::Engine::Components::Identity
93
+
94
+ WIDTH = 640
95
+ HEIGHT = 480
96
+ ASSETS = File.expand_path('../assets', __dir__)
97
+ LOCALES = File.expand_path('locales', __dir__) # the text on screen: locales/en.yml
98
+
99
+ DOG_SPEED = 150.0
100
+ SHEEP_SPEED = 35.0
101
+ STARTING_FLOCK = 5
102
+ DEFAULT_SEED = 0x5EED2
103
+
104
+ # One sheep: a position, an id, and wool that grows while it stands there.
105
+ #
106
+ # The wool is what makes this a *record* rather than a coordinate pair. A save
107
+ # that wrote only positions would restore a flock of freshly shorn sheep, and
108
+ # nobody would notice until they wondered why shearing never seemed to pay.
109
+ class Sheep < RGame::Engine::Node2D
110
+ FLEECE = RGame::Util::Color.new(240, 240, 235)
111
+ INK = RGame::Util::Color.new(30, 40, 30)
112
+ MIN_R = 7.0
113
+ MAX_WOOL = 9.0
114
+ GROWTH = 1.4 # radius per second
115
+
116
+ attr_reader :id
117
+ attr_accessor :wool
118
+
119
+ def initialize(id:, wool: 0.0, **)
120
+ super(**)
121
+ @id = id
122
+ @wool = wool
123
+ # Built once, on the way in. `renderer.text(@id.to_s, …)` would allocate a
124
+ # String every frame for every sheep, which is what
125
+ # Game/NoInterpolationInHotPath is about even though `to_s` is not
126
+ # interpolation.
127
+ @label = id.to_s
128
+ add_component(Identity.new(id: id))
129
+ end
130
+
131
+ def on_update(dt)
132
+ @wool = [@wool + (dt * GROWTH), MAX_WOOL].min
133
+ self.x = x.clamp(0, WIDTH)
134
+ self.y = y.clamp(40, HEIGHT)
135
+ end
136
+
137
+ def on_draw(renderer, _view)
138
+ renderer.circle(0, 0, MIN_R + @wool, color: FLEECE)
139
+ renderer.text(@label, MIN_R + MAX_WOOL + 4, -8, color: INK)
140
+ end
141
+ end
142
+
143
+ class Pasture < RGame::Engine::Node2D
144
+ GRASS = RGame::Util::Color.new(96, 140, 84)
145
+ DOG = RGame::Util::Color.new(70, 50, 40)
146
+ TETHER = RGame::Util::Color.new(250, 240, 180)
147
+ DOG_R = 11
148
+
149
+ STATUS = { fresh: RGame::Engine::Text.new('status.fresh'),
150
+ restored: RGame::Engine::Text.new('status.restored'),
151
+ saved: RGame::Engine::Text.new('status.saved'),
152
+ loaded: RGame::Engine::Text.new('status.loaded'),
153
+ deleted: RGame::Engine::Text.new('status.deleted') }.freeze
154
+
155
+ def initialize(save:, **)
156
+ super(**)
157
+ @save = save
158
+ @help = RGame::Engine::Text.new('help.keys')
159
+ @save_keys = RGame::Engine::Text.new('help.save')
160
+ @rng = Random.new(ENV.fetch('RGAME_SEED', DEFAULT_SEED).to_i)
161
+ @flock = []
162
+ @next_id = 1
163
+ @status = :fresh
164
+ end
165
+
166
+ def on_add
167
+ @dog = add_node(build_dog)
168
+ STARTING_FLOCK.times { spawn_sheep }
169
+ @target = @flock.first
170
+
171
+ return unless @save.exist?
172
+
173
+ load_state
174
+ @status = :restored
175
+ end
176
+
177
+ def on_control(actions)
178
+ target_next if actions.pressed?(:target_next)
179
+ shear if actions.pressed?(:shear)
180
+ spawn_sheep if actions.pressed?(:spawn)
181
+
182
+ save_state if actions.pressed?(:save)
183
+ load_state if actions.pressed?(:load)
184
+ drop_save if actions.pressed?(:drop)
185
+ end
186
+
187
+ def on_draw(renderer, view)
188
+ renderer.rect(0, 0, view.width, view.height, color: GRASS)
189
+ # Between two nodes, so the scene draws it: it is the only thing that knows
190
+ # both. The sheep draw themselves, over this.
191
+ renderer.line(@dog.x, @dog.y, @target.x, @target.y, color: TETHER) if @target
192
+ renderer.circle(@dog.x, @dog.y, DOG_R, color: DOG)
193
+
194
+ renderer.text(@help, 12, 12)
195
+ renderer.text(@save_keys, 12, 34)
196
+ renderer.text(STATUS.fetch(@status), 12, 56)
197
+ end
198
+
199
+ private
200
+
201
+ # --- the save ----------------------------------------------------------
202
+ #
203
+ # Records, not coordinates: each sheep writes everything about itself that the
204
+ # next run cannot work out for itself.
205
+
206
+ def save_state
207
+ @save.write(
208
+ next_id: @next_id,
209
+ dog: [@dog.x, @dog.y],
210
+ target: Identity.of(@target), # a node becomes an id, here and nowhere else
211
+ sheep: @flock.map { |sheep| { id: sheep.id, x: sheep.x, y: sheep.y, wool: sheep.wool } }
212
+ )
213
+ @status = :saved
214
+ end
215
+
216
+ def load_state
217
+ state = @save.read
218
+ return if state.empty?
219
+
220
+ @flock.each(&:queue_free)
221
+ @flock = []
222
+ state.fetch(:sheep, []).each { |record| restore_sheep(record) }
223
+ # After the flock, because the target has to be found among the sheep that
224
+ # now exist rather than the ones that used to.
225
+ restore_target(state[:target])
226
+ @dog.x, @dog.y = state[:dog] if state[:dog]
227
+ # Restored, not recomputed. A counter that started again from the flock size
228
+ # would collide with any id belonging to a sheep that has since been sheared.
229
+ @next_id = state.fetch(:next_id, @flock.size + 1)
230
+ @status = :loaded
231
+ end
232
+
233
+ def restore_sheep(record)
234
+ @flock << add_node(build_sheep(record.fetch(:id), record.fetch(:x), record.fetch(:y),
235
+ wool: record.fetch(:wool, 0.0)))
236
+ end
237
+
238
+ # The re-link, and the whole reason ids are here. `find` rather than an index:
239
+ # the flock that comes back is a different set of objects in whatever order the
240
+ # file listed them.
241
+ def restore_target(id)
242
+ @target = @flock.find { |sheep| sheep.id == id } || @flock.first
243
+ end
244
+
245
+ def drop_save
246
+ @save.delete
247
+ @status = :deleted
248
+ end
249
+
250
+ # --- the game ----------------------------------------------------------
251
+
252
+ def target_next
253
+ return if @flock.empty?
254
+
255
+ index = @flock.index(@target) || -1
256
+ @target = @flock[(index + 1) % @flock.size]
257
+ end
258
+
259
+ def shear
260
+ return if @target.nil?
261
+
262
+ @target.wool = 0.0
263
+ sheared = @target
264
+ @flock.delete(sheared)
265
+ sheared.queue_free
266
+ # The removed sheep leaves a hole in the middle of the flock, and every id
267
+ # after it keeps the number it had. That is the property an array index
268
+ # cannot give you.
269
+ @target = @flock.first
270
+ end
271
+
272
+ def spawn_sheep
273
+ id = @next_id
274
+ @next_id += 1
275
+ @flock << add_node(build_sheep(id, @rng.rand(WIDTH - 80) + 40, @rng.rand(HEIGHT - 120) + 80))
276
+ @target = @flock.first if @target.nil?
277
+ end
278
+
279
+ def build_sheep(id, x, y, wool: 0.0)
280
+ sheep = Sheep.new(id: id, wool: wool, x: x, y: y)
281
+ sheep.add_component(RGame::Engine::Components::CharacterBody.new(speed: SHEEP_SPEED))
282
+ sheep.add_component(RGame::Engine::Components::WanderController.new(rng: @rng))
283
+ sheep
284
+ end
285
+
286
+ def build_dog
287
+ dog = RGame::Engine::Node2D.new(x: WIDTH / 2, y: HEIGHT / 2)
288
+ dog.add_component(RGame::Engine::Components::CharacterBody.new(speed: DOG_SPEED))
289
+ dog.add_component(RGame::Engine::Components::PlayerController.new)
290
+ dog
291
+ end
292
+ end
293
+
294
+ save = RGame::Util::SaveFile.new('flock.json', game: 'rgame-examples',
295
+ dir: ENV.fetch('RGAME_SAVE_DIR', nil))
296
+
297
+ game = RGame::Game.new(
298
+ root: Pasture.new(save: save),
299
+ caption: 'Save and load with ids',
300
+ width: WIDTH,
301
+ height: HEIGHT,
302
+ media_root: ASSETS,
303
+ locales: LOCALES,
304
+ # F5 and F9 rather than S and L: **a key already in the default map keeps
305
+ # doing its default job too.** `move_y` is bound to W and S, so a save action
306
+ # on S would save *and* walk the dog downwards — two actions may read one key,
307
+ # both fire, and nothing warns. F1 and F2 are not free either; RGame::Game
308
+ # keeps them for the debug overlay and quit.
309
+ #
310
+ # Space is a deliberate exception: the default map has it on `fire` and
311
+ # `ui_confirm`, and nothing here reads either, so sharing it costs nothing.
312
+ input_map: RGame::Engine::InputMap.default.merge(
313
+ target_next: { buttons: [Controls::KEY_TAB] },
314
+ shear: { buttons: [Controls::KEY_SPACE] },
315
+ spawn: { buttons: [Controls::KEY_N] },
316
+ save: { buttons: [Controls::KEY_F5] },
317
+ load: { buttons: [Controls::KEY_F9] },
318
+ drop: { buttons: [Controls::KEY_DELETE] }
319
+ )
320
+ )
321
+
322
+ game.start
@@ -0,0 +1,4 @@
1
+ # The English table: every string this example draws.
2
+ en:
3
+ help:
4
+ scroll: Arrow keys / WASD / gamepad to scroll
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Scroll map — a Tiled map, larger than the window, that a player scrolls.
4
+ #
5
+ # Run it:
6
+ #
7
+ # ruby examples/scroll_map/main.rb
8
+ #
9
+ # Arrow keys, WASD, a d-pad or a left stick pan the view; the map stops at its
10
+ # own edges. It exercises:
11
+ # - the `:tilemap` asset loader — a .tmx becomes a parsed map plus a renderer;
12
+ # - Components::TileWorld — the scene-scoped system that owns the map and the
13
+ # world's size;
14
+ # - TileMapLayer.mount — one node per Tiled layer, drawn as world content;
15
+ # - WorldView — where world space begins, and where the camera is applied;
16
+ # - Camera — clamped to the world, so the view never shows past the edge;
17
+ # - Components::CameraFollow — what points a camera at something.
18
+ #
19
+ # ## Moving a camera means moving a node
20
+ #
21
+ # There is no "pan the camera" call, and the absence is the design. A camera is
22
+ # owned by a *player* — a scene may have any number of viewers, so it cannot own
23
+ # one — and where it points is decided by a CameraFollow component on some node
24
+ # in the world. Scrolling is therefore the same thing as walking: build a node,
25
+ # drive it with the usual controller, and hang a camera off it.
26
+ #
27
+ # That node is the Rig below. It is `examples/walk`'s hero with the sprite left
28
+ # off — same CharacterBody, same PlayerController — which is the point worth
29
+ # taking away: the camera is not a special case, and a game that follows its
30
+ # player is this file with a sprite added back.
31
+
32
+ $LOAD_PATH.unshift File.expand_path('../../lib', __dir__)
33
+ require 'rgame/game'
34
+
35
+ WIDTH = 640
36
+ HEIGHT = 480
37
+ ASSETS = File.expand_path('../assets', __dir__)
38
+ LOCALES = File.expand_path('locales', __dir__) # the text on screen: locales/en.yml
39
+
40
+ MAP = 'town.tmx'
41
+ SPEED = 220.0 # px/s — a camera pans faster than a character walks
42
+
43
+ # The node the camera follows: no sprite, no collision, just a position that
44
+ # input moves and a camera that trails it.
45
+ #
46
+ # It draws a crosshair so the centre of the view is visible, which is the only
47
+ # reason it is a class rather than a bare Node2D with three components on it.
48
+ class Rig < RGame::Engine::Node2D
49
+ ARM = 7
50
+ # A Color rather than an array literal, and a constant rather than either: a
51
+ # drawing call coerces `nil`, `[r, g, b]` or a Color, and building the array
52
+ # inline would allocate one every frame. RGame::Util::Color is a value type —
53
+ # frozen, comparable, and safe to share.
54
+ TINT = RGame::Util::Color.new(250, 250, 255)
55
+
56
+ def initialize(world_width:, world_height:, **)
57
+ super(**)
58
+ @world_width = world_width
59
+ @world_height = world_height
60
+ end
61
+
62
+ # Keep the rig on the map. The camera already refuses to show past the world's
63
+ # edges, but the rig is not the camera: without this it walks off into
64
+ # nothing, the view stays pinned to the edge, and the player is left pushing a
65
+ # key that does nothing visible. Clamping both to the same bounds keeps the
66
+ # crosshair and the view agreeing.
67
+ def on_update(_dt)
68
+ self.x = x.clamp(0, @world_width)
69
+ self.y = y.clamp(0, @world_height)
70
+ end
71
+
72
+ # (0, 0) is this node: Node2D#draw has already pushed its transform, and the
73
+ # WorldView above has already applied the camera. Reading `x` or `world_x`
74
+ # here would apply one of those a second time.
75
+ def on_draw(renderer, _view)
76
+ renderer.line(-ARM, 0, ARM, 0, color: TINT)
77
+ renderer.line(0, -ARM, 0, ARM, color: TINT)
78
+ end
79
+ end
80
+
81
+ # The scene: mount the map, mount the world, put the rig in it.
82
+ class Scene < RGame::Engine::Node2D
83
+ def initialize
84
+ super
85
+ @help = RGame::Engine::Text.new('help.scroll')
86
+ end
87
+
88
+ def on_add
89
+ # `.tilemap` is a loader RGame::Game installs, because building one needs
90
+ # both layers at once: Engine::TileMap reads the .tmx, and the renderer that
91
+ # draws it is Core. What comes back holds both; `.map` is the grid half.
92
+ map = root.context.assets.tilemap(MAP).map
93
+ players = root.system(RGame::Engine::Players)
94
+
95
+ # The system every actor asks about the world: how big it is, what is solid.
96
+ # Handing it the cameras is what bounds them — a camera left unbounded
97
+ # follows its target exactly and will happily show the void past the edge.
98
+ add_component(RGame::Engine::Components::TileWorld.new(
99
+ map: map, tilemap_id: MAP, cameras: players.map(&:camera)
100
+ ))
101
+
102
+ # World space begins here. Everything under it is drawn in world
103
+ # coordinates, once per viewport, through that viewport's camera.
104
+ view = add_node(RGame::Engine::WorldView.new)
105
+ # One node per Tiled layer. The node handed back is the gap between the
106
+ # ground layers and any layer Tiled flags `above`, which is where things
107
+ # that walk around go — the rig included, so a canopy layer would pass over
108
+ # it without this file choosing a single z.
109
+ actors = RGame::Engine::TileMapLayer.mount(view)
110
+ actors.add_node(build_rig(map, players.primary.camera))
111
+ end
112
+
113
+ def on_draw(renderer, _view)
114
+ # Outside the WorldView, so this is screen space: it stays put while the
115
+ # world scrolls under it.
116
+ renderer.text(@help, 12, 12)
117
+ end
118
+
119
+ private
120
+
121
+ def build_rig(map, camera)
122
+ rig = Rig.new(x: map.pixel_width / 2.0, y: map.pixel_height / 2.0,
123
+ world_width: map.pixel_width, world_height: map.pixel_height)
124
+ rig.add_component(RGame::Engine::Components::CharacterBody.new(speed: SPEED))
125
+ rig.add_component(RGame::Engine::Components::PlayerController.new)
126
+ rig.add_component(RGame::Engine::Components::CameraFollow.new(camera: camera))
127
+ rig
128
+ end
129
+ end
130
+
131
+ game = RGame::Game.new(
132
+ root: Scene.new,
133
+ caption: 'Scroll map',
134
+ width: WIDTH,
135
+ height: HEIGHT,
136
+ media_root: ASSETS,
137
+ locales: LOCALES
138
+ )
139
+
140
+ game.start
@@ -0,0 +1,6 @@
1
+ # The English table: every string this example draws.
2
+ en:
3
+ help:
4
+ plate: Enter stands on the plate — the door and the lamp both answer
5
+ unnamed: Neither of them is named anywhere inside the plate
6
+ cooldowns: "Hold Space and hold E: one ActionTrigger, two cooldowns"