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
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# The English table: every string this example draws.
|
|
2
|
+
en:
|
|
3
|
+
help:
|
|
4
|
+
seat: Press A on a controller to hand it the seat
|
|
5
|
+
back: Enter or Space on the keyboard takes it back
|
|
6
|
+
devices:
|
|
7
|
+
keyboard: Keyboard
|
|
8
|
+
controller: Controller
|
|
9
|
+
# What this game calls each action. The button shown beside it changes with
|
|
10
|
+
# the device; the word does not.
|
|
11
|
+
actions:
|
|
12
|
+
ui_confirm: Confirm
|
|
13
|
+
ui_cancel: Cancel
|
|
14
|
+
fire: Wave
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Input glyphs — the prompt matches the thing in your hand.
|
|
4
|
+
#
|
|
5
|
+
# Run it:
|
|
6
|
+
#
|
|
7
|
+
# ruby examples/input_glyphs/main.rb
|
|
8
|
+
#
|
|
9
|
+
# Arrow keys, WASD or a stick walk the hero. Press **A** on a controller and the
|
|
10
|
+
# panel switches to controller prompts; press **Enter** or **Space** on the
|
|
11
|
+
# keyboard and it switches back. It exercises:
|
|
12
|
+
# - Controls.gamepad? — is the device in the player's hands a controller;
|
|
13
|
+
# - InputMap#button_for — which of an action's ids that device can press;
|
|
14
|
+
# - Engine::Players with one seat — `on_unassigned_input` defaulting to
|
|
15
|
+
# `:takeover`, which is what keyboard-to-controller switching *is*;
|
|
16
|
+
# - renderer.sprite — a glyph sheet indexed by button id.
|
|
17
|
+
#
|
|
18
|
+
# ## Nothing here listens for a controller being plugged in
|
|
19
|
+
#
|
|
20
|
+
# A game with one seat treats an unassigned device that somebody uses as *that
|
|
21
|
+
# player* picking it up, rather than as a second player arriving. So plugging a
|
|
22
|
+
# pad in does nothing at all, and pressing A on it hands the seat over: the
|
|
23
|
+
# player's `device` changes, and every prompt below follows it on the next frame.
|
|
24
|
+
#
|
|
25
|
+
# That is the engine's `:takeover` policy, and a one-seat game gets it without
|
|
26
|
+
# asking. `examples/split_screen` is the other setting of the same dial.
|
|
27
|
+
#
|
|
28
|
+
# **It switches back, through `ui_confirm` and nothing else.** Once the pad has
|
|
29
|
+
# the seat the keyboard is unassigned, so Enter or Space hands it back — but W
|
|
30
|
+
# does not, and neither does any other key. One action rather than "any input" is
|
|
31
|
+
# deliberate: a stick resting a little off centre must never count as somebody
|
|
32
|
+
# picking up a controller, and the rule is the same rule in both directions. A
|
|
33
|
+
# game that genuinely wants any key to return to the keyboard sets
|
|
34
|
+
# `players.on_unassigned_input = :ignore` and calls `players.seat(device)` on its
|
|
35
|
+
# own terms.
|
|
36
|
+
#
|
|
37
|
+
# ## One entry lists both, and a prompt has to undo that
|
|
38
|
+
#
|
|
39
|
+
# An `InputMap` entry names the key *and* the pad button for one action:
|
|
40
|
+
#
|
|
41
|
+
# fire: { buttons: [Controls::KEY_SPACE, Controls::PAD_A] }
|
|
42
|
+
#
|
|
43
|
+
# That is right for reading input — a device only answers for its own kind of id,
|
|
44
|
+
# so polling needs no branch — and exactly wrong for showing it. A prompt saying
|
|
45
|
+
# "press Space or A" tells the player about hardware they are not holding.
|
|
46
|
+
#
|
|
47
|
+
# `InputMap#button_for(action, device)` is the question the other way round: of
|
|
48
|
+
# the ids bound to this action, which one can *this* device press? It compares
|
|
49
|
+
# `Controls.pad_button?(id)` against `Controls.gamepad?(device)` — two id spaces
|
|
50
|
+
# that were made disjoint for exactly this — and gives back the first that
|
|
51
|
+
# matches, or nil.
|
|
52
|
+
#
|
|
53
|
+
# The first match is not arbitrary: `ui_confirm` lists Return before Space, so
|
|
54
|
+
# the panel says Return. The order an entry is written in is the order a prompt
|
|
55
|
+
# prefers.
|
|
56
|
+
#
|
|
57
|
+
# ## A glyph is a button id rendered as a picture
|
|
58
|
+
#
|
|
59
|
+
# Which makes the whole of it a lookup keyed by that id:
|
|
60
|
+
#
|
|
61
|
+
# GLYPH_COLUMN = { Controls::KEY_SPACE => 0, ... }.freeze
|
|
62
|
+
#
|
|
63
|
+
# The sheet is one row of 64x64 frames, drawn with `renderer.sprite`, and the
|
|
64
|
+
# table says which column belongs to which id. There is no cleverness to it, and
|
|
65
|
+
# that is the point — the hard part was deciding *which* id to draw, and that
|
|
66
|
+
# happened above.
|
|
67
|
+
#
|
|
68
|
+
# The table covers the five ids this example's three actions can produce. A game
|
|
69
|
+
# with a rebinding screen needs one entry per id it lets a player bind, or a text
|
|
70
|
+
# fallback for the ones it has no art for; here a missing glyph draws nothing and
|
|
71
|
+
# leaves the label to speak for itself.
|
|
72
|
+
#
|
|
73
|
+
# ## What it does not solve
|
|
74
|
+
#
|
|
75
|
+
# **Axes.** Walking is bound to `move_x`/`move_y`, which are a stick and pairs of
|
|
76
|
+
# keys rather than buttons, so `button_for` gives nil for them and the panel does
|
|
77
|
+
# not try: the picture for "the left stick" or "the arrow keys" is one image
|
|
78
|
+
# standing for several ids, which is a different question with a different
|
|
79
|
+
# answer. The three prompts here are all single buttons.
|
|
80
|
+
#
|
|
81
|
+
# **Which controller.** The pad glyphs are Xbox-shaped because SDL's button names
|
|
82
|
+
# are (`PAD_A`, `PAD_B`), and a PlayStation pad reports the same ids under
|
|
83
|
+
# different labels. Shipping a second sheet and choosing between them by
|
|
84
|
+
# controller name is a real thing games do, and it is another table keyed by the
|
|
85
|
+
# same ids.
|
|
86
|
+
|
|
87
|
+
$LOAD_PATH.unshift File.expand_path('../../lib', __dir__)
|
|
88
|
+
require 'rgame/game'
|
|
89
|
+
|
|
90
|
+
WIDTH = 640
|
|
91
|
+
HEIGHT = 480
|
|
92
|
+
ASSETS = File.expand_path('../assets', __dir__)
|
|
93
|
+
LOCALES = File.expand_path('locales', __dir__) # the text on screen: locales/en.yml
|
|
94
|
+
|
|
95
|
+
SPEED = 90.0
|
|
96
|
+
|
|
97
|
+
# The glyph sheet: one row of 64x64 frames, in the order examples/assets/README.md
|
|
98
|
+
# lists them.
|
|
99
|
+
SHEET = 'glyphs.json'
|
|
100
|
+
GLYPH = 64
|
|
101
|
+
|
|
102
|
+
# A prompt is a picture of a button, so the table is keyed by button id. These
|
|
103
|
+
# five are what this example's three actions resolve to on the two kinds of
|
|
104
|
+
# device.
|
|
105
|
+
GLYPH_COLUMN = {
|
|
106
|
+
RGame::Util::Controls::KEY_SPACE => 0,
|
|
107
|
+
RGame::Util::Controls::KEY_RETURN => 1,
|
|
108
|
+
RGame::Util::Controls::KEY_ESCAPE => 2,
|
|
109
|
+
RGame::Util::Controls::PAD_A => 3,
|
|
110
|
+
RGame::Util::Controls::PAD_B => 4
|
|
111
|
+
}.freeze
|
|
112
|
+
|
|
113
|
+
# The panel: three actions, and the button each one is on for the device the
|
|
114
|
+
# player is holding right now.
|
|
115
|
+
class Prompts < RGame::Engine::Node2D
|
|
116
|
+
PANEL = RGame::Util::Color.new(18, 22, 30, 220)
|
|
117
|
+
INK = RGame::Util::Color.new(228, 232, 240)
|
|
118
|
+
DIM = RGame::Util::Color.new(150, 158, 172)
|
|
119
|
+
|
|
120
|
+
# The action to ask about, and what this game calls it. Both fixed: the label
|
|
121
|
+
# is the game's word for the action, and the button is what changes.
|
|
122
|
+
ROWS = { ui_confirm: RGame::Engine::Text.new('actions.ui_confirm'),
|
|
123
|
+
ui_cancel: RGame::Engine::Text.new('actions.ui_cancel'),
|
|
124
|
+
fire: RGame::Engine::Text.new('actions.fire') }.freeze
|
|
125
|
+
|
|
126
|
+
# A text chosen by state rather than built from it: one Text per state, so there
|
|
127
|
+
# is nothing to render again but a language switch.
|
|
128
|
+
DEVICE_NAME = { false => RGame::Engine::Text.new('devices.keyboard'),
|
|
129
|
+
true => RGame::Engine::Text.new('devices.controller') }.freeze
|
|
130
|
+
|
|
131
|
+
WIDTH_PX = 300
|
|
132
|
+
ROW_H = 72
|
|
133
|
+
PAD = 14
|
|
134
|
+
HEADER_H = 34
|
|
135
|
+
|
|
136
|
+
def on_add
|
|
137
|
+
# One seat, so the player is the primary one. Held rather than looked up per
|
|
138
|
+
# frame: which player this is cannot change, while the device they hold can.
|
|
139
|
+
@player = system(RGame::Engine::Players).primary
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def on_draw(renderer, _view)
|
|
143
|
+
device = @player.device
|
|
144
|
+
renderer.rect(0, 0, WIDTH_PX, HEADER_H + (ROWS.size * ROW_H), color: PANEL)
|
|
145
|
+
renderer.text(DEVICE_NAME.fetch(RGame::Util::Controls.gamepad?(device)), PAD, PAD, color: INK)
|
|
146
|
+
|
|
147
|
+
ROWS.each_with_index do |(action, label), index|
|
|
148
|
+
draw_row(renderer, action, label, device, HEADER_H + (index * ROW_H))
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
private
|
|
153
|
+
|
|
154
|
+
# hot-path
|
|
155
|
+
def draw_row(renderer, action, label, device, y)
|
|
156
|
+
id = @player.input_map.button_for(action, device)
|
|
157
|
+
column = GLYPH_COLUMN[id]
|
|
158
|
+
renderer.sprite(SHEET, 0, column, PAD, y) unless column.nil?
|
|
159
|
+
renderer.text(label, PAD + GLYPH + PAD, y + ((GLYPH - 16) / 2), color: id.nil? ? DIM : INK)
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Something to walk, so that picking up a controller has a reason to happen.
|
|
164
|
+
# `examples/walk`'s hero, kept inside the window.
|
|
165
|
+
class Hero < RGame::Engine::Node2D
|
|
166
|
+
def initialize(**)
|
|
167
|
+
super
|
|
168
|
+
add_component(RGame::Engine::Components::AnimatedSprite.new(sheet: 'hero.json'))
|
|
169
|
+
add_component(RGame::Engine::Components::CharacterBody.new(speed: SPEED))
|
|
170
|
+
add_component(RGame::Engine::Components::PlayerController.new)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def on_update(_dt)
|
|
174
|
+
self.x = x.clamp(0, WIDTH - width)
|
|
175
|
+
self.y = y.clamp(0, HEIGHT - height)
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
class Scene < RGame::Engine::Node2D
|
|
180
|
+
BACKDROP = RGame::Util::Color.new(30, 36, 46)
|
|
181
|
+
MARGIN = 16
|
|
182
|
+
|
|
183
|
+
# The hero first, so the panel is drawn over them rather than under: a node
|
|
184
|
+
# draws before its later siblings, and the two do overlap once the hero is
|
|
185
|
+
# walked into the corner.
|
|
186
|
+
def initialize
|
|
187
|
+
super
|
|
188
|
+
@help_seat = RGame::Engine::Text.new('help.seat')
|
|
189
|
+
@help_back = RGame::Engine::Text.new('help.back')
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def on_add
|
|
193
|
+
add_node(Hero.new(x: 470, y: 300))
|
|
194
|
+
add_node(Prompts.new(x: MARGIN, y: MARGIN))
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def on_draw(renderer, view)
|
|
198
|
+
renderer.rect(0, 0, view.width, view.height, color: BACKDROP)
|
|
199
|
+
renderer.text(@help_seat, MARGIN, view.height - 52)
|
|
200
|
+
renderer.text(@help_back, MARGIN, view.height - 30)
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
game = RGame::Game.new(
|
|
205
|
+
root: Scene.new,
|
|
206
|
+
caption: 'Input glyphs',
|
|
207
|
+
width: WIDTH,
|
|
208
|
+
height: HEIGHT,
|
|
209
|
+
media_root: ASSETS,
|
|
210
|
+
locales: LOCALES
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
game.start
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# The English table: every string this example draws.
|
|
2
|
+
en:
|
|
3
|
+
help:
|
|
4
|
+
walk: Arrows / WASD / gamepad to walk, Space / A to hop
|
|
5
|
+
box: The red box stays on the ground. So does everything it collides with
|
|
6
|
+
fence: "Walk south into the fence and hop: the picture clears it, the feet do not"
|
|
7
|
+
status:
|
|
8
|
+
airborne: In the air
|
|
9
|
+
grounded: On the ground
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Jump top-down — a hop that leaves the ground without leaving its spot on it.
|
|
4
|
+
#
|
|
5
|
+
# Run it:
|
|
6
|
+
#
|
|
7
|
+
# ruby examples/jump_topdown/main.rb
|
|
8
|
+
#
|
|
9
|
+
# Arrow keys, WASD, a d-pad or a left stick walk the hero; Space or the pad's A
|
|
10
|
+
# button hops. The trees and the fence are solid. It exercises:
|
|
11
|
+
# - Components::Hop — a parabola in time, written to the node's elevation;
|
|
12
|
+
# - Node2D#elevation — how far a node's picture is drawn above where it stands;
|
|
13
|
+
# - Components::AnimatedSprite — the picture that rises with it;
|
|
14
|
+
# - Components::FeetCollider and Components::CharacterBody — the shape and the
|
|
15
|
+
# steps that stay on the ground, from `examples/collision_tiles`;
|
|
16
|
+
# - Components::TileWorld and Components::CameraFollow — the map and a camera
|
|
17
|
+
# that does not bob;
|
|
18
|
+
# - InputMap.default.merge — the :jump action this game declares.
|
|
19
|
+
#
|
|
20
|
+
# ## A jump is a drawing offset
|
|
21
|
+
#
|
|
22
|
+
# In a top-down view, "up" on the screen is *north*. So a jump cannot move the
|
|
23
|
+
# node up the screen: that would be walking north, and the feet box would go with
|
|
24
|
+
# it, into whatever is north of the hero. What leaves the ground is the picture.
|
|
25
|
+
#
|
|
26
|
+
# `Hop` writes its height to `node.elevation`, and `elevation` is not part of the
|
|
27
|
+
# transform: `y` does not change, `world_y` does not change, the feet box does not
|
|
28
|
+
# move, and neither does the camera following them. `AnimatedSprite` is the one
|
|
29
|
+
# thing that reads it, and draws the sprite that far above the spot. Watch the red
|
|
30
|
+
# box during a hop — it never leaves the floor, and the shadow sits on it, getting
|
|
31
|
+
# smaller as the hero gets further away from it.
|
|
32
|
+
#
|
|
33
|
+
# ## So a hop does not clear a fence
|
|
34
|
+
#
|
|
35
|
+
# Walk up to the fence and hop over it. The sprite rises well above it, and the
|
|
36
|
+
# hero stops exactly where walking stopped them, because what collides never
|
|
37
|
+
# left the ground. That is correct for a hop and would be wrong for a vault, and
|
|
38
|
+
# the difference is a decision about the map rather than about the jump.
|
|
39
|
+
#
|
|
40
|
+
# ## What this does not solve
|
|
41
|
+
#
|
|
42
|
+
# **Crossing anything.** `hop.airborne?` is the seam for that: a game whose
|
|
43
|
+
# chasm tiles should be passable in the air reads it where it decides what is
|
|
44
|
+
# solid. `TileWorld` does not consult it, and `Hop` knows nothing about tiles.
|
|
45
|
+
# Which tiles a jump clears, and what happens to a hero who lands on one, are
|
|
46
|
+
# rules of a particular game, and a jump component that had an opinion about
|
|
47
|
+
# them would be wrong for the next game.
|
|
48
|
+
#
|
|
49
|
+
# **Height against the scenery.** The lifted sprite keeps the draw order it has
|
|
50
|
+
# on the ground, so a hop next to a tree still passes behind the canopy that
|
|
51
|
+
# would hide the hero walking there. A game with tall jumps would sort by the
|
|
52
|
+
# feet and draw the lifted part over the scenery.
|
|
53
|
+
|
|
54
|
+
$LOAD_PATH.unshift File.expand_path('../../lib', __dir__)
|
|
55
|
+
require 'rgame/game'
|
|
56
|
+
|
|
57
|
+
WIDTH = 640
|
|
58
|
+
HEIGHT = 480
|
|
59
|
+
ASSETS = File.expand_path('../assets', __dir__)
|
|
60
|
+
LOCALES = File.expand_path('locales', __dir__) # the text on screen: locales/en.yml
|
|
61
|
+
|
|
62
|
+
MAP = 'town.tmx'
|
|
63
|
+
SPEED = 80.0 # px/s
|
|
64
|
+
|
|
65
|
+
# A hop about as tall as the hero's head, over half a second: enough to read as a
|
|
66
|
+
# jump at this sprite size, short enough that it is over before the next press.
|
|
67
|
+
HOP_PEAK = 18.0
|
|
68
|
+
HOP_DURATION = 0.5
|
|
69
|
+
|
|
70
|
+
FEET_WIDTH = 12
|
|
71
|
+
FEET_HEIGHT = 6
|
|
72
|
+
|
|
73
|
+
CAMERA_OFFSET_X = 8
|
|
74
|
+
CAMERA_OFFSET_Y = 19
|
|
75
|
+
|
|
76
|
+
# North of the fence, two tiles above it: one walk down reaches it.
|
|
77
|
+
START_X = 384.0
|
|
78
|
+
START_Y = 264.0
|
|
79
|
+
|
|
80
|
+
Controls = RGame::Util::Controls
|
|
81
|
+
|
|
82
|
+
# A walker from `examples/collision_tiles` with one more component. The only
|
|
83
|
+
# drawing it does itself is what stays on the ground: the shadow and the feet box.
|
|
84
|
+
class Hero < RGame::Engine::Node2D
|
|
85
|
+
FEET = RGame::Util::Color.rgba(255, 110, 110, 120)
|
|
86
|
+
SHADOW = RGame::Util::Color.rgba(0, 0, 0, 90)
|
|
87
|
+
|
|
88
|
+
# The shadow on the ground is an ellipse this wide and tall, and shrinks to half
|
|
89
|
+
# its size once the hero is this far above it.
|
|
90
|
+
SHADOW_RADIUS_X = 7.0
|
|
91
|
+
SHADOW_RADIUS_Y = 3.0
|
|
92
|
+
SHADOW_HALF_AT = 18.0
|
|
93
|
+
|
|
94
|
+
attr_reader :hop
|
|
95
|
+
|
|
96
|
+
def initialize(camera:, **)
|
|
97
|
+
super(**)
|
|
98
|
+
add_component(RGame::Engine::Components::AnimatedSprite.new(sheet: 'hero.json'))
|
|
99
|
+
@collider = add_component(RGame::Engine::Components::FeetCollider.new(
|
|
100
|
+
width: FEET_WIDTH, height: FEET_HEIGHT
|
|
101
|
+
))
|
|
102
|
+
add_component(RGame::Engine::Components::CharacterBody.new(speed: SPEED, blocked_by: [:tiles]))
|
|
103
|
+
add_component(RGame::Engine::Components::PlayerController.new)
|
|
104
|
+
# The one line that differs from `examples/collision_tiles`. It needs nothing
|
|
105
|
+
# from its siblings and none of them know it is here: it writes the node's
|
|
106
|
+
# elevation, and the sprite already draws lifted by whatever that is.
|
|
107
|
+
@hop = add_component(RGame::Engine::Components::Hop.new(peak: HOP_PEAK, duration: HOP_DURATION))
|
|
108
|
+
add_component(RGame::Engine::Components::CameraFollow.new(
|
|
109
|
+
camera: camera, offset_x: CAMERA_OFFSET_X, offset_y: CAMERA_OFFSET_Y
|
|
110
|
+
))
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Both drawn at the feet, and neither reads the node's elevation: this is the
|
|
114
|
+
# part of the hero that is on the ground. The shadow is a circle squashed into an
|
|
115
|
+
# ellipse, so it is placed first and squashed second — squashing first would
|
|
116
|
+
# squash the placement too. It shrinks through the circle's radius rather than
|
|
117
|
+
# the squash, which keeps the ellipse's proportions. `z: -1` puts it under the
|
|
118
|
+
# sprite, which draws at 0.
|
|
119
|
+
def on_draw(renderer, _view)
|
|
120
|
+
box = @collider.box
|
|
121
|
+
shrink = 1.0 / (1.0 + (@hop.height / SHADOW_HALF_AT))
|
|
122
|
+
renderer.translated(box.offset_x + (box.width / 2.0), box.offset_y + (box.height / 2.0)) do
|
|
123
|
+
renderer.scaled(SHADOW_RADIUS_X, SHADOW_RADIUS_Y) do
|
|
124
|
+
renderer.circle(0, 0, shrink, color: SHADOW, z: -1)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
renderer.rect(box.offset_x, box.offset_y, box.width, box.height, color: FEET)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# The scene from `examples/collision_tiles`, less the spiky ball.
|
|
132
|
+
class Scene < RGame::Engine::Node2D
|
|
133
|
+
STATE = { true => RGame::Engine::Text.new('status.airborne'),
|
|
134
|
+
false => RGame::Engine::Text.new('status.grounded') }.freeze
|
|
135
|
+
|
|
136
|
+
def initialize
|
|
137
|
+
super
|
|
138
|
+
@help_walk = RGame::Engine::Text.new('help.walk')
|
|
139
|
+
@help_box = RGame::Engine::Text.new('help.box')
|
|
140
|
+
@help_fence = RGame::Engine::Text.new('help.fence')
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def on_add
|
|
144
|
+
map = root.context.assets.tilemap(MAP).map
|
|
145
|
+
players = root.system(RGame::Engine::Players)
|
|
146
|
+
|
|
147
|
+
add_component(RGame::Engine::Components::TileWorld.new(
|
|
148
|
+
map: map, tilemap_id: MAP, cameras: players.map(&:camera)
|
|
149
|
+
))
|
|
150
|
+
|
|
151
|
+
view = add_node(RGame::Engine::WorldView.new)
|
|
152
|
+
actors = RGame::Engine::TileMapLayer.mount(view)
|
|
153
|
+
@hero = actors.add_node(Hero.new(camera: players.primary.camera, x: START_X, y: START_Y))
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def on_draw(renderer, _view)
|
|
157
|
+
renderer.text(@help_walk, 12, 12)
|
|
158
|
+
renderer.text(@help_box, 12, 34)
|
|
159
|
+
renderer.text(@help_fence, 12, 56)
|
|
160
|
+
renderer.text(STATE[@hero.hop.airborne?], 12, 78)
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
game = RGame::Game.new(
|
|
165
|
+
root: Scene.new,
|
|
166
|
+
caption: 'Jump top-down',
|
|
167
|
+
width: WIDTH,
|
|
168
|
+
height: HEIGHT,
|
|
169
|
+
media_root: ASSETS,
|
|
170
|
+
locales: LOCALES,
|
|
171
|
+
# :jump is this game's own action. Space is also in the default map as :fire and
|
|
172
|
+
# :ui_confirm, which nothing in this scene reads.
|
|
173
|
+
input_map: RGame::Engine::InputMap.default.merge(
|
|
174
|
+
jump: { buttons: [Controls::KEY_SPACE, Controls::PAD_A] }
|
|
175
|
+
)
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
game.start
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# The German table. It has no `zero:`, because German says "0 Äpfel", and it
|
|
2
|
+
# lacks `hud.hint` on purpose: main.rb's header says what that shows.
|
|
3
|
+
de:
|
|
4
|
+
title: Lokalisierung
|
|
5
|
+
hud:
|
|
6
|
+
apples:
|
|
7
|
+
one: "%{count} Apfel"
|
|
8
|
+
other: "%{count} Äpfel"
|
|
9
|
+
locale: "Aktuelles Gebietsschema: %{locale}"
|
|
10
|
+
language:
|
|
11
|
+
title: Sprache
|
|
12
|
+
system: Systemsprache verwenden
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# The English table, and the default: a key German lacks shows this text.
|
|
2
|
+
en:
|
|
3
|
+
title: Localization
|
|
4
|
+
hud:
|
|
5
|
+
apples:
|
|
6
|
+
zero: No apples
|
|
7
|
+
one: "%{count} apple"
|
|
8
|
+
other: "%{count} apples"
|
|
9
|
+
locale: "Current locale: %{locale}"
|
|
10
|
+
hint: Left and right change the count
|
|
11
|
+
language:
|
|
12
|
+
title: Language
|
|
13
|
+
system: Use the system language
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Localization — the same screen in two languages.
|
|
4
|
+
#
|
|
5
|
+
# Run it:
|
|
6
|
+
#
|
|
7
|
+
# ruby examples/localization/main.rb
|
|
8
|
+
#
|
|
9
|
+
# Left and right change the number of apples. Up and down move through the
|
|
10
|
+
# language menu, and Enter picks one. Quit and run it again: the language you
|
|
11
|
+
# picked is still the one on screen. It exercises:
|
|
12
|
+
# - Engine::I18n — tables in locales/, a fallback to English, CLDR plurals;
|
|
13
|
+
# - Engine::Text — a key built once, with a `count` and with a variable;
|
|
14
|
+
# - UI::Menu's `scope:`, and `Text.literal` for text that is never translated;
|
|
15
|
+
# - RGame::Game's `locales:`, and the language it picks from the OS;
|
|
16
|
+
# - Util::SaveFile, holding the language a player picked.
|
|
17
|
+
#
|
|
18
|
+
# ## Nothing is looked up while drawing
|
|
19
|
+
#
|
|
20
|
+
# Every string on screen is an Engine::Text built in `initialize`. Reading one
|
|
21
|
+
# compares its variables and `I18n.generation` with what it last rendered, and
|
|
22
|
+
# returns the same String while nothing changed. Picking a language moves the
|
|
23
|
+
# generation, so every Text and every button renders again on its next read,
|
|
24
|
+
# and nothing here tells them to.
|
|
25
|
+
#
|
|
26
|
+
# ## The tables sit beside this file
|
|
27
|
+
#
|
|
28
|
+
# `Game.new(locales: File.join(__dir__, 'locales'))` loads `en.yml` and `de.yml`
|
|
29
|
+
# from here. Every example shares `examples/assets/` as its media root, so a
|
|
30
|
+
# table there would be loaded into every example.
|
|
31
|
+
#
|
|
32
|
+
# ## German lacks a key, on purpose
|
|
33
|
+
#
|
|
34
|
+
# `de.yml` has no `hud.hint`. In German the hint shows the English text, because
|
|
35
|
+
# German's fallback chain ends at the default locale, English. A player reads a
|
|
36
|
+
# sentence in a language the game ships rather than `hud.hint`. That is the right
|
|
37
|
+
# failure for a game, and the wrong one to ship on purpose: a project from
|
|
38
|
+
# `rgame new` has a locales spec that fails until German has the key.
|
|
39
|
+
#
|
|
40
|
+
# German also has no `zero:` for the apples. English says "No apples", German
|
|
41
|
+
# says "0 Äpfel": an explicit `zero:` belongs to the table that has it.
|
|
42
|
+
#
|
|
43
|
+
# ## Language names are literals
|
|
44
|
+
#
|
|
45
|
+
# "English" and "Deutsch" are `Text.literal`, not keys. A language is named in
|
|
46
|
+
# its own language whichever one is current, so its name must not translate.
|
|
47
|
+
# The third button is a key, and the menu's `scope: 'language'` makes `system`
|
|
48
|
+
# read `language.system`. A literal has no key, so the scope does not touch it.
|
|
49
|
+
#
|
|
50
|
+
# ## What this does not solve
|
|
51
|
+
#
|
|
52
|
+
# The engine layer cannot measure text yet, so the buttons are a fixed width,
|
|
53
|
+
# chosen wide enough for the longer label in either language: "Use the system
|
|
54
|
+
# language". A third language with longer words would need a wider slot.
|
|
55
|
+
|
|
56
|
+
$LOAD_PATH.unshift File.expand_path('../../lib', __dir__)
|
|
57
|
+
require 'rgame/game'
|
|
58
|
+
|
|
59
|
+
UI = RGame::Engine::UI
|
|
60
|
+
I18n = RGame::Engine::I18n
|
|
61
|
+
Text = RGame::Engine::Text
|
|
62
|
+
|
|
63
|
+
WIDTH = 640
|
|
64
|
+
HEIGHT = 360
|
|
65
|
+
ASSETS = File.expand_path('../assets', __dir__)
|
|
66
|
+
LOCALES = File.expand_path('locales', __dir__)
|
|
67
|
+
|
|
68
|
+
# The language on screen, and the file that remembers a player's choice.
|
|
69
|
+
#
|
|
70
|
+
# `RGame::Game` picks a language from the OS before `start`. A choice saved
|
|
71
|
+
# here wins over that, and "use the system language" forgets the choice again.
|
|
72
|
+
class Language
|
|
73
|
+
def initialize(save, preferred)
|
|
74
|
+
@save = save
|
|
75
|
+
@preferred = preferred
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Called between `Game.new`, which loaded the tables, and `start`. `choose`
|
|
79
|
+
# takes the saved locale if a table covers it, and the OS's otherwise, so a
|
|
80
|
+
# hand-edited file holding a language this game lacks changes nothing.
|
|
81
|
+
def restore
|
|
82
|
+
saved = @save.read[:language]
|
|
83
|
+
I18n.locale = I18n.choose([saved, *@preferred]) if saved.is_a?(String) && !saved.empty?
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def pick(locale)
|
|
87
|
+
I18n.locale = locale
|
|
88
|
+
@save.write(language: locale.name)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def follow_system
|
|
92
|
+
@save.delete
|
|
93
|
+
I18n.locale = I18n.choose(@preferred)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# The whole screen: a heading, three lines of text and the language menu.
|
|
98
|
+
class Screen < RGame::Engine::Node2D
|
|
99
|
+
MARGIN = 40
|
|
100
|
+
MENU_Y = 196
|
|
101
|
+
ITEM_WIDTH = 280
|
|
102
|
+
ITEM_HEIGHT = 34
|
|
103
|
+
|
|
104
|
+
def initialize(language:, **)
|
|
105
|
+
super(**)
|
|
106
|
+
@language = language
|
|
107
|
+
@apples_held = 3
|
|
108
|
+
@title = Text.new('title')
|
|
109
|
+
@apples = Text.new('hud.apples', :count)
|
|
110
|
+
@locale = Text.new('hud.locale', :locale)
|
|
111
|
+
@hint = Text.new('hud.hint')
|
|
112
|
+
@heading = Text.new('language.title')
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def on_add
|
|
116
|
+
menu = add_node(UI::Menu.new(x: MARGIN, y: MENU_Y, scope: 'language',
|
|
117
|
+
layout: UI::Column.new(item_width: ITEM_WIDTH, item_height: ITEM_HEIGHT)))
|
|
118
|
+
menu.add(UI::PanelButton.new(label: Text.literal('English'))).on_activated { @language.pick(:en) }
|
|
119
|
+
menu.add(UI::PanelButton.new(label: Text.literal('Deutsch'))).on_activated { @language.pick(:de) }
|
|
120
|
+
menu.add(UI::PanelButton.new(label: 'system')).on_activated { @language.follow_system }
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def on_control(actions)
|
|
124
|
+
@apples_held += 1 if actions.pressed?(:ui_right)
|
|
125
|
+
@apples_held -= 1 if actions.pressed?(:ui_left) && @apples_held.positive?
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def on_draw(renderer, _view)
|
|
129
|
+
renderer.text(@title, MARGIN, 24)
|
|
130
|
+
renderer.text(@apples.with(count: @apples_held), MARGIN, 72)
|
|
131
|
+
renderer.text(@locale.with(locale: I18n.locale.name), MARGIN, 96)
|
|
132
|
+
renderer.text(@hint, MARGIN, 120)
|
|
133
|
+
renderer.text(@heading, MARGIN, MENU_Y - 30)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# `dir:` is normally left out and the file lands in the platform's own data
|
|
138
|
+
# directory. It is overridable so a driven run writes somewhere disposable.
|
|
139
|
+
save = RGame::Util::SaveFile.new('language.json', game: 'rgame-examples',
|
|
140
|
+
dir: ENV.fetch('RGAME_SAVE_DIR', nil))
|
|
141
|
+
language = Language.new(save, RGame::Core.preferred_locales)
|
|
142
|
+
|
|
143
|
+
game = RGame::Game.new(
|
|
144
|
+
root: Screen.new(language: language),
|
|
145
|
+
caption: 'Localization',
|
|
146
|
+
width: WIDTH,
|
|
147
|
+
height: HEIGHT,
|
|
148
|
+
media_root: ASSETS,
|
|
149
|
+
locales: LOCALES
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
language.restore
|
|
153
|
+
|
|
154
|
+
# The buttons draw nine-slices, which name elements of an atlas rather than
|
|
155
|
+
# files, so the atlas is registered before the first frame.
|
|
156
|
+
game.renderer.register_ui_atlas(game.assets.ui_atlas('ui.json'))
|
|
157
|
+
|
|
158
|
+
game.start
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# The English table: every string this example draws.
|
|
2
|
+
en:
|
|
3
|
+
title:
|
|
4
|
+
heading: Menu navigation
|
|
5
|
+
help: Up and down to move, Enter to choose
|
|
6
|
+
play: Play
|
|
7
|
+
settings: Settings
|
|
8
|
+
quit: Quit
|
|
9
|
+
settings:
|
|
10
|
+
heading: Settings — left and right change a value
|
|
11
|
+
fullscreen: Fullscreen
|
|
12
|
+
scale: Scale mode
|
|
13
|
+
volume: Volume
|
|
14
|
+
back: Back
|
|
15
|
+
# Quoted: YAML reads a bare on and off as true and false.
|
|
16
|
+
'on': "on"
|
|
17
|
+
'off': "off"
|
|
18
|
+
disabled: disabled
|
|
19
|
+
stretch: stretch
|
|
20
|
+
letterbox: letterbox
|
|
21
|
+
integer: integer
|
|
22
|
+
play:
|
|
23
|
+
help: Enter makes a noise — Escape returns to the title
|