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