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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +135 -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 +2 -3
- 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 +152 -11
- 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,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RGame
|
|
4
|
+
module Engine
|
|
5
|
+
module UI
|
|
6
|
+
# A menu that draws its own backdrop: a nine-slice round its buttons,
|
|
7
|
+
# grown by `padding` on every side.
|
|
8
|
+
#
|
|
9
|
+
# column = UI::Column.new(item_width: 180, item_height: 34)
|
|
10
|
+
# menu = layer.add_node(UI::PanelMenu.new(x: 56, y: 56, layout: column))
|
|
11
|
+
# menu.add(UI::PanelButton.new(label: 'Resume')).on_activated { close }
|
|
12
|
+
#
|
|
13
|
+
# The panel is sized from the menu's bounds, which the layout recomputes on
|
|
14
|
+
# every `add`, so a button added later grows the panel with it — there is
|
|
15
|
+
# no count to keep in step with the buttons. The menu's origin stays the
|
|
16
|
+
# layout's origin: with a UI::Column that is the first button's corner, and
|
|
17
|
+
# the panel reaches `padding` beyond it up and to the left.
|
|
18
|
+
#
|
|
19
|
+
# The buttons are children, so they draw over the panel without a `z`.
|
|
20
|
+
class PanelMenu < Menu
|
|
21
|
+
attr_reader :panel, :padding
|
|
22
|
+
|
|
23
|
+
def initialize(panel: :panel, padding: 16, **)
|
|
24
|
+
super(**)
|
|
25
|
+
@panel = panel
|
|
26
|
+
@padding = padding
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def on_draw(renderer, _view)
|
|
30
|
+
renderer.nine_slice(@panel, bounds_x - @padding, bounds_y - @padding,
|
|
31
|
+
bounds_width + (@padding * 2), bounds_height + (@padding * 2))
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RGame
|
|
4
|
+
module Engine
|
|
5
|
+
module UI
|
|
6
|
+
# Focus chosen by direction: the button a stick points at is the focused
|
|
7
|
+
# one. Built for a UI::Ring, which is what makes a radial menu.
|
|
8
|
+
#
|
|
9
|
+
# wheel = UI::Menu.new(x: 320, y: 240,
|
|
10
|
+
# layout: UI::Ring.new(radius: 120, item_width: 96, item_height: 30),
|
|
11
|
+
# navigation: UI::Pointing.new)
|
|
12
|
+
#
|
|
13
|
+
# It reads `ui_radial_x` and `ui_radial_y` from the universal set every
|
|
14
|
+
# InputMap is merged over. They share the left stick with `move_x` /
|
|
15
|
+
# `move_y` by default and are still separate actions, so a game can move
|
|
16
|
+
# its wheel to the right stick without rebinding how its players walk.
|
|
17
|
+
#
|
|
18
|
+
# ## The button pointed at is the nearest by angle
|
|
19
|
+
#
|
|
20
|
+
# Each button's centre, seen from the menu's origin, is a direction, and the
|
|
21
|
+
# stick's direction focuses whichever button's direction is closest. On a
|
|
22
|
+
# ring centred on the origin that cuts the circle into one equal sector per
|
|
23
|
+
# button, centred on it. It asks the layout nothing: the angles come from
|
|
24
|
+
# where the buttons actually are, so a ring that starts somewhere else, or a
|
|
25
|
+
# layout that is not a ring, cannot disagree with it.
|
|
26
|
+
#
|
|
27
|
+
# ## Below the dead zone nothing is focused
|
|
28
|
+
#
|
|
29
|
+
# A stick at rest is a direction too, just a meaningless one, and a
|
|
30
|
+
# released stick springs back through the middle on its way there. So a
|
|
31
|
+
# deflection shorter than `dead_zone` focuses nothing, and a confirm in
|
|
32
|
+
# that state activates nothing — letting go of the stick and pressing A
|
|
33
|
+
# never picks whatever happened to be under it last.
|
|
34
|
+
#
|
|
35
|
+
# That dead zone is a *length*, on the combined vector, and sits on top of
|
|
36
|
+
# the per-axis one ActionMapper applies: that one takes 0.15 off each axis
|
|
37
|
+
# and rescales, and exists to stop a worn stick drifting. It is far too
|
|
38
|
+
# small to decide that a player means a direction.
|
|
39
|
+
#
|
|
40
|
+
# A disabled button is never focused, so pointing at one selects nothing.
|
|
41
|
+
#
|
|
42
|
+
# ## A grace window, for a stick that springs back
|
|
43
|
+
#
|
|
44
|
+
# `grace:` is how long, in seconds, focus survives the stick entering the
|
|
45
|
+
# dead zone before it clears. A player choosing by letting go of a
|
|
46
|
+
# trigger lets go of the stick a moment earlier, and the stick is back in
|
|
47
|
+
# the middle before the trigger comes up; the window is what lets that
|
|
48
|
+
# release still choose. Leaving the stick at rest for longer than it
|
|
49
|
+
# chooses nothing, which is the dead-zone rule again, only later.
|
|
50
|
+
#
|
|
51
|
+
# It applies to the dead zone only: pointing at a disabled button clears
|
|
52
|
+
# focus at once. Time is counted in `update(dt)`, so a paused menu's window
|
|
53
|
+
# does not run out.
|
|
54
|
+
#
|
|
55
|
+
# `GRACE`, 0.15 s, is nine frames at 60 Hz. A released stick is back in the
|
|
56
|
+
# middle within a frame or two and overshoots for about 50 ms more before
|
|
57
|
+
# its spring settles; the window covers both, plus the moment between a
|
|
58
|
+
# thumb leaving the stick and a finger leaving the shoulder button. Much
|
|
59
|
+
# longer, and centring the stick to cancel starts to feel like waiting.
|
|
60
|
+
class Pointing < Navigation
|
|
61
|
+
DEAD_ZONE = 0.5
|
|
62
|
+
GRACE = 0.15
|
|
63
|
+
|
|
64
|
+
# `grace` is what was passed, or once the menu is built, the resolved
|
|
65
|
+
# default. nil before then when none was passed.
|
|
66
|
+
attr_reader :dead_zone, :grace, :aim_x, :aim_y
|
|
67
|
+
|
|
68
|
+
# `grace: nil` resolves when the menu is built: GRACE on a menu with a
|
|
69
|
+
# trigger, which is chosen by letting go, and 0.0 on any other.
|
|
70
|
+
def initialize(dead_zone: DEAD_ZONE, grace: nil)
|
|
71
|
+
super()
|
|
72
|
+
@dead_zone = dead_zone
|
|
73
|
+
@grace = grace
|
|
74
|
+
@aim_x = 0.0
|
|
75
|
+
@aim_y = 0.0
|
|
76
|
+
@at_rest = false
|
|
77
|
+
@at_rest_for = 0.0
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def attach(menu)
|
|
81
|
+
super
|
|
82
|
+
@grace = menu.trigger ? GRACE : 0.0 if @grace.nil?
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# The index of the button `(x, y)` points at, or nil if the vector is
|
|
86
|
+
# shorter than the dead zone. `y` is positive downwards, like the stick
|
|
87
|
+
# and the screen. Whether that button is enabled is not this method's
|
|
88
|
+
# question.
|
|
89
|
+
def index_at(x, y)
|
|
90
|
+
return nil if Math.hypot(x, y) < @dead_zone
|
|
91
|
+
|
|
92
|
+
aim = Math.atan2(y, x)
|
|
93
|
+
nearest = nil
|
|
94
|
+
best = Float::INFINITY
|
|
95
|
+
menu.buttons.each_index do |index|
|
|
96
|
+
gap = angle_between(aim, direction_of(menu.buttons[index]))
|
|
97
|
+
next unless gap < best
|
|
98
|
+
|
|
99
|
+
best = gap
|
|
100
|
+
nearest = index
|
|
101
|
+
end
|
|
102
|
+
nearest
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def on_control(actions)
|
|
106
|
+
@aim_x = actions.axis(:ui_radial_x)
|
|
107
|
+
@aim_y = actions.axis(:ui_radial_y)
|
|
108
|
+
if Math.hypot(@aim_x, @aim_y) < @dead_zone
|
|
109
|
+
@at_rest_for = 0.0 unless @at_rest
|
|
110
|
+
@at_rest = true
|
|
111
|
+
menu.focus(nil) unless @at_rest_for < @grace
|
|
112
|
+
else
|
|
113
|
+
@at_rest = false
|
|
114
|
+
index = index_at(@aim_x, @aim_y)
|
|
115
|
+
menu.focus(index && menu.buttons[index].enabled? ? index : nil)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Counts how long the stick has been at rest, for the grace window.
|
|
120
|
+
def update(dt)
|
|
121
|
+
@at_rest_for += dt if @at_rest
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Forgets the last aim and focuses nothing, so a menu opened again does
|
|
125
|
+
# not start where the last opening left off.
|
|
126
|
+
def on_opened
|
|
127
|
+
@aim_x = 0.0
|
|
128
|
+
@aim_y = 0.0
|
|
129
|
+
@at_rest = false
|
|
130
|
+
menu.focus(nil)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
private
|
|
134
|
+
|
|
135
|
+
def direction_of(button)
|
|
136
|
+
Math.atan2(button.y + (button.height / 2.0), button.x + (button.width / 2.0))
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def angle_between(first, second)
|
|
140
|
+
gap = (first - second).abs % (Math::PI * 2)
|
|
141
|
+
gap > Math::PI ? (Math::PI * 2) - gap : gap
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RGame
|
|
4
|
+
module Engine
|
|
5
|
+
module UI
|
|
6
|
+
# A wheel: a Menu with its buttons on a UI::Ring and focus chosen by
|
|
7
|
+
# UI::Pointing, drawing its own backdrop disc, the dead zone to scale, and
|
|
8
|
+
# a pointer from the centre towards where the stick aims.
|
|
9
|
+
#
|
|
10
|
+
# wheel = layer.add_node(UI::RadialMenu.new(x: 320, y: 240, radius: 150, button_width: 64))
|
|
11
|
+
# wheel.add(UI::IconButton.new(image: :home, style: UI::ShapeStyle.new(shape: :disc)))
|
|
12
|
+
#
|
|
13
|
+
# The menu's origin is the centre of the wheel. The backdrop is sized from
|
|
14
|
+
# the ring's bounds — the square enclosing every slot, whatever the number
|
|
15
|
+
# of buttons — plus `padding`, so it does not change size as buttons are
|
|
16
|
+
# added. The pointer's tip is clamped to the ring: two arrow keys read as
|
|
17
|
+
# (1, 1), which is longer than a stick can reach.
|
|
18
|
+
#
|
|
19
|
+
# `backdrop:`, `dead_zone_color:` and `pointer:` are colours, and `nil`
|
|
20
|
+
# omits that part. The buttons are children, so they draw over all three.
|
|
21
|
+
#
|
|
22
|
+
# `trigger:` makes it the wheel held open by a button — see UI::Menu, "A
|
|
23
|
+
# menu held open by an action" — and `grace:` is its UI::Pointing's, which
|
|
24
|
+
# already defaults to `Pointing::GRACE` on a menu with a trigger.
|
|
25
|
+
#
|
|
26
|
+
# It builds its own layout and navigation, so passing `layout:` or
|
|
27
|
+
# `navigation:` raises ArgumentError: forwarded on, either would silently
|
|
28
|
+
# replace the ring or the pointing this class is made of. A wheel stepped
|
|
29
|
+
# through with the default navigation is a UI::Menu with a UI::Ring.
|
|
30
|
+
class RadialMenu < Menu
|
|
31
|
+
BACKDROP = Util::Color.new(44, 40, 52)
|
|
32
|
+
DEAD_ZONE = Util::Color.new(76, 72, 88)
|
|
33
|
+
POINTER = Util::Color.new(240, 236, 224)
|
|
34
|
+
|
|
35
|
+
POINTER_THICKNESS = 3.0
|
|
36
|
+
POINTER_TIP_RADIUS = 6
|
|
37
|
+
|
|
38
|
+
attr_reader :padding, :backdrop, :dead_zone_color, :pointer
|
|
39
|
+
|
|
40
|
+
def initialize(radius:, button_width:, button_height: button_width, dead_zone: Pointing::DEAD_ZONE,
|
|
41
|
+
grace: nil, padding: 16, backdrop: BACKDROP, dead_zone_color: DEAD_ZONE, pointer: POINTER,
|
|
42
|
+
**options)
|
|
43
|
+
refuse_preset_keywords(options)
|
|
44
|
+
super(layout: Ring.new(radius: radius, item_width: button_width, item_height: button_height),
|
|
45
|
+
navigation: Pointing.new(dead_zone: dead_zone, grace: grace), **options)
|
|
46
|
+
@padding = padding
|
|
47
|
+
@backdrop = backdrop&.then { Util::Color.coerce(it) }
|
|
48
|
+
@dead_zone_color = dead_zone_color&.then { Util::Color.coerce(it) }
|
|
49
|
+
@pointer = pointer&.then { Util::Color.coerce(it) }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def on_draw(renderer, _view)
|
|
53
|
+
radius = layout.radius
|
|
54
|
+
renderer.circle(0, 0, backdrop_radius, color: @backdrop) if @backdrop
|
|
55
|
+
renderer.circle(0, 0, navigation.dead_zone * radius, color: @dead_zone_color) if @dead_zone_color
|
|
56
|
+
draw_pointer(renderer, radius) if @pointer
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# The radius of the disc drawn behind the ring: half the larger side of
|
|
60
|
+
# the bounds, plus `padding`.
|
|
61
|
+
def backdrop_radius
|
|
62
|
+
([bounds_width, bounds_height].max / 2.0) + @padding
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def refuse_preset_keywords(options)
|
|
68
|
+
return unless options.key?(:layout) || options.key?(:navigation)
|
|
69
|
+
|
|
70
|
+
raise ArgumentError, 'a RadialMenu builds its own layout: and navigation:; use UI::Menu to choose them'
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def draw_pointer(renderer, radius)
|
|
74
|
+
aim_x = navigation.aim_x
|
|
75
|
+
aim_y = navigation.aim_y
|
|
76
|
+
reach = radius / [Math.hypot(aim_x, aim_y), 1.0].max
|
|
77
|
+
tip_x = aim_x * reach
|
|
78
|
+
tip_y = aim_y * reach
|
|
79
|
+
renderer.line(0, 0, tip_x, tip_y, thickness: POINTER_THICKNESS, color: @pointer)
|
|
80
|
+
renderer.circle(tip_x, tip_y, POINTER_TIP_RADIUS, color: @pointer)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RGame
|
|
4
|
+
module Engine
|
|
5
|
+
module UI
|
|
6
|
+
# A Menu layout: items spaced evenly round a circle **centred on the
|
|
7
|
+
# menu's origin**, the first straight up and the rest clockwise.
|
|
8
|
+
#
|
|
9
|
+
# UI::Menu.new(x: 320, y: 240, navigation: UI::Pointing.new,
|
|
10
|
+
# layout: UI::Ring.new(radius: 120, item_width: 96, item_height: 30))
|
|
11
|
+
#
|
|
12
|
+
# Each item is centred on its point of the circle, so `radius` is the
|
|
13
|
+
# distance from the menu's origin to an item's middle. Adding an item
|
|
14
|
+
# re-spaces every item, because the gap between them depends on how many
|
|
15
|
+
# there are.
|
|
16
|
+
#
|
|
17
|
+
# Paired with UI::Pointing it is a radial menu. With the default
|
|
18
|
+
# UI::Stepping it is a ring that `ui_up` and `ui_down` step round, which is
|
|
19
|
+
# why its `axis` is `:vertical`.
|
|
20
|
+
class Ring
|
|
21
|
+
attr_reader :radius, :item_width, :item_height
|
|
22
|
+
|
|
23
|
+
def axis = :vertical
|
|
24
|
+
|
|
25
|
+
def initialize(radius:, item_width:, item_height:)
|
|
26
|
+
@radius = radius
|
|
27
|
+
@item_width = item_width
|
|
28
|
+
@item_height = item_height
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def arrange(items)
|
|
32
|
+
count = items.size
|
|
33
|
+
items.each_with_index do |item, index|
|
|
34
|
+
angle = Math::PI * 2 * index / count
|
|
35
|
+
item.x = (Math.sin(angle) * @radius) - (@item_width / 2.0)
|
|
36
|
+
item.y = (-Math.cos(angle) * @radius) - (@item_height / 2.0)
|
|
37
|
+
item.width = @item_width
|
|
38
|
+
item.height = @item_height
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# `[x, y, width, height]` of the square that encloses the whole circle
|
|
43
|
+
# of slots, centred on the menu's origin — not the tightest box round
|
|
44
|
+
# the items placed so far, so a backdrop keeps its size as a ring grows.
|
|
45
|
+
# All zero for no items.
|
|
46
|
+
def bounds(items)
|
|
47
|
+
return [0, 0, 0, 0] if items.empty?
|
|
48
|
+
|
|
49
|
+
[-@radius - (@item_width / 2.0), -@radius - (@item_height / 2.0),
|
|
50
|
+
(@radius * 2) + @item_width, (@radius * 2) + @item_height]
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RGame
|
|
4
|
+
module Engine
|
|
5
|
+
module UI
|
|
6
|
+
# A Menu layout: buttons side by side rightwards from the menu's origin,
|
|
7
|
+
# all the same size, `spacing` pixels apart — a skill bar, a row of tabs.
|
|
8
|
+
# A UI::Stack on the horizontal axis, so the default UI::Stepping moves
|
|
9
|
+
# through it with `ui_left` and `ui_right`, with nothing else said.
|
|
10
|
+
#
|
|
11
|
+
# UI::Menu.new(layout: UI::Row.new(item_width: 64, item_height: 64))
|
|
12
|
+
#
|
|
13
|
+
# It takes no `axis:` — a row that is not horizontal is a UI::Column.
|
|
14
|
+
class Row < Stack
|
|
15
|
+
def initialize(item_width:, item_height:, spacing: 8)
|
|
16
|
+
super(axis: :horizontal, item_width: item_width, item_height: item_height, spacing: spacing)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RGame
|
|
4
|
+
module Engine
|
|
5
|
+
module UI
|
|
6
|
+
# A button background drawn from plain shapes — a rectangle or a disc, a
|
|
7
|
+
# fill per state, and an outline while focused or pressed — so a menu can
|
|
8
|
+
# be built and played before any art exists.
|
|
9
|
+
#
|
|
10
|
+
# round = UI::ShapeStyle.new(shape: :disc)
|
|
11
|
+
# quiet = UI::ShapeStyle.new(colors: UI::ShapeStyle::COLORS.merge(idle: nil), outline: nil)
|
|
12
|
+
#
|
|
13
|
+
# The fill is inset by `border` in every state, and the outline is the whole
|
|
14
|
+
# shape drawn under it, so a button does not change size as its state
|
|
15
|
+
# changes: focus only uncovers the ring the fill leaves. A disc is centred in
|
|
16
|
+
# the slot, as large as its shorter side.
|
|
17
|
+
#
|
|
18
|
+
# `nil` in `colors:` draws no fill in that state, and `outline: nil` draws no
|
|
19
|
+
# outline. Colours are coerced once, here, so `colors:` takes arrays and a
|
|
20
|
+
# draw allocates nothing; and a missing state is a `KeyError` here, rather
|
|
21
|
+
# than on the first frame a button happens to be disabled.
|
|
22
|
+
#
|
|
23
|
+
# Fill and outline are drawn at `z: 0` and `z: -1`. Shapes default to
|
|
24
|
+
# `z: 50`, which would cover the label or icon a button draws at `z: 1`.
|
|
25
|
+
#
|
|
26
|
+
# ## What reads on the fill is the style's to say
|
|
27
|
+
#
|
|
28
|
+
# `content:` is the colour a button draws its label or icon in over each
|
|
29
|
+
# state's fill, or `nil` to keep the button's own. The pressed fill is gold,
|
|
30
|
+
# and so is IconButton's pressed tint, so without it a pressed icon is gold
|
|
31
|
+
# on gold and vanishes; a light label nearly does. The style picks the fill,
|
|
32
|
+
# so it is the one place that can pick what goes on it. Defaults to dark
|
|
33
|
+
# while pressed and the button's own colour otherwise.
|
|
34
|
+
class ShapeStyle
|
|
35
|
+
SHAPES = %i[rect disc].freeze
|
|
36
|
+
OUTLINED = %i[focused pressed].freeze
|
|
37
|
+
|
|
38
|
+
COLORS = {
|
|
39
|
+
idle: Util::Color.new(52, 48, 62),
|
|
40
|
+
focused: Util::Color.new(72, 66, 88),
|
|
41
|
+
pressed: Util::Color.new(240, 200, 96),
|
|
42
|
+
disabled: Util::Color.new(40, 38, 46)
|
|
43
|
+
}.freeze
|
|
44
|
+
OUTLINE = Util::Color.new(240, 200, 96)
|
|
45
|
+
CONTENT = { idle: nil, focused: nil, pressed: Util::Color.new(46, 34, 24), disabled: nil }.freeze
|
|
46
|
+
|
|
47
|
+
FILL_Z = 0
|
|
48
|
+
OUTLINE_Z = -1
|
|
49
|
+
|
|
50
|
+
attr_reader :shape, :colors, :outline, :border, :content
|
|
51
|
+
|
|
52
|
+
def initialize(shape: :rect, colors: COLORS, outline: OUTLINE, border: 3, content: CONTENT)
|
|
53
|
+
unless SHAPES.include?(shape)
|
|
54
|
+
raise ArgumentError, "shape: must be one of #{SHAPES.inspect}, not #{shape.inspect}"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
@shape = shape
|
|
58
|
+
@colors = Button::STATES.to_h { |state| [state, colors.fetch(state)&.then { Util::Color.coerce(it) }] }.freeze
|
|
59
|
+
@content = Button::STATES.to_h do |state|
|
|
60
|
+
[state, content.fetch(state)&.then { Util::Color.coerce(it) }]
|
|
61
|
+
end.freeze
|
|
62
|
+
@outline = outline&.then { Util::Color.coerce(it) }
|
|
63
|
+
@border = border
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
DEFAULT = new
|
|
67
|
+
|
|
68
|
+
# The colour a button draws its content in over this state's fill, or
|
|
69
|
+
# nil for the button's own. See "What reads on the fill" above.
|
|
70
|
+
def content_color(state) = @content.fetch(state)
|
|
71
|
+
|
|
72
|
+
def draw(renderer, state, width, height)
|
|
73
|
+
outlined = @outline && OUTLINED.include?(state)
|
|
74
|
+
fill = @colors.fetch(state)
|
|
75
|
+
if @shape == :disc
|
|
76
|
+
draw_disc(renderer, fill, outlined, width, height)
|
|
77
|
+
else
|
|
78
|
+
draw_rect(renderer, fill, outlined, width, height)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def draw_rect(renderer, fill, outlined, width, height)
|
|
85
|
+
renderer.rect(0, 0, width, height, z: OUTLINE_Z, color: @outline) if outlined
|
|
86
|
+
return unless fill
|
|
87
|
+
|
|
88
|
+
renderer.rect(@border, @border, width - (@border * 2), height - (@border * 2),
|
|
89
|
+
z: FILL_Z, color: fill)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def draw_disc(renderer, fill, outlined, width, height)
|
|
93
|
+
cx = width / 2.0
|
|
94
|
+
cy = height / 2.0
|
|
95
|
+
radius = [width, height].min / 2.0
|
|
96
|
+
renderer.circle(cx, cy, radius, z: OUTLINE_Z, color: @outline) if outlined
|
|
97
|
+
renderer.circle(cx, cy, radius - @border, z: FILL_Z, color: fill) if fill
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RGame
|
|
4
|
+
module Engine
|
|
5
|
+
module UI
|
|
6
|
+
# A Menu layout: buttons in a line from the menu's origin, all the same
|
|
7
|
+
# size, `spacing` pixels apart — downwards along the `:vertical` axis,
|
|
8
|
+
# rightwards along the `:horizontal` one. UI::Column and UI::Row are this
|
|
9
|
+
# with the axis fixed, and are what a game builds.
|
|
10
|
+
#
|
|
11
|
+
# A layout is anything that answers `arrange(buttons)` by setting each
|
|
12
|
+
# button's position and size, relative to the menu, `bounds(buttons)` with
|
|
13
|
+
# the rectangle that encloses them, and `axis` — the direction UI::Stepping
|
|
14
|
+
# moves focus along unless told otherwise. The Menu calls `arrange` and
|
|
15
|
+
# `bounds` every time a button is added, so a layout whose places depend on
|
|
16
|
+
# how many buttons there are — UI::Ring — re-spaces them all. It holds no
|
|
17
|
+
# state about any one menu, so one instance may serve several.
|
|
18
|
+
class Stack
|
|
19
|
+
AXES = %i[vertical horizontal].freeze
|
|
20
|
+
|
|
21
|
+
attr_reader :axis, :item_width, :item_height, :spacing
|
|
22
|
+
|
|
23
|
+
def initialize(axis:, item_width:, item_height:, spacing: 8)
|
|
24
|
+
raise ArgumentError, "axis: must be one of #{AXES.inspect}, not #{axis.inspect}" unless AXES.include?(axis)
|
|
25
|
+
|
|
26
|
+
@axis = axis
|
|
27
|
+
@item_width = item_width
|
|
28
|
+
@item_height = item_height
|
|
29
|
+
@spacing = spacing
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def arrange(buttons)
|
|
33
|
+
vertical = @axis == :vertical
|
|
34
|
+
step = (vertical ? @item_height : @item_width) + @spacing
|
|
35
|
+
buttons.each_with_index do |button, index|
|
|
36
|
+
button.x = vertical ? 0 : index * step
|
|
37
|
+
button.y = vertical ? index * step : 0
|
|
38
|
+
button.width = @item_width
|
|
39
|
+
button.height = @item_height
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# `[x, y, width, height]` of the buttons in their line, relative to the
|
|
44
|
+
# menu: all zero for no buttons, since no gap sits before the first.
|
|
45
|
+
def bounds(buttons)
|
|
46
|
+
count = buttons.size
|
|
47
|
+
return [0, 0, 0, 0] if count.zero?
|
|
48
|
+
|
|
49
|
+
if @axis == :vertical
|
|
50
|
+
[0, 0, @item_width, (count * @item_height) + ((count - 1) * @spacing)]
|
|
51
|
+
else
|
|
52
|
+
[0, 0, (count * @item_width) + ((count - 1) * @spacing), @item_height]
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RGame
|
|
4
|
+
module Engine
|
|
5
|
+
module UI
|
|
6
|
+
# Focus that moves one button at a time, along an axis: on a vertical one
|
|
7
|
+
# `ui_up` and `ui_down` step through the buttons in the order they were
|
|
8
|
+
# added and `ui_left` / `ui_right` go to the focused button; on a
|
|
9
|
+
# horizontal one the two pairs swap. Menu's default navigation.
|
|
10
|
+
#
|
|
11
|
+
# UI::Menu.new(layout: UI::Column.new(item_width: 220, item_height: 44)) # up and down
|
|
12
|
+
# UI::Menu.new(layout: UI::Row.new(item_width: 64, item_height: 64)) # left and right
|
|
13
|
+
#
|
|
14
|
+
# **The axis is the layout's** unless one is passed, so a UI::Row steps
|
|
15
|
+
# with left and right with nothing else said. `Stepping.new(axis:
|
|
16
|
+
# :horizontal)` overrides it — a UI::Ring stepped round with left and
|
|
17
|
+
# right, say. It is read once, when the menu is built.
|
|
18
|
+
#
|
|
19
|
+
# **Focus is never empty** while the menu has a button. It starts on the
|
|
20
|
+
# first enabled one, skips disabled ones as it moves, and wraps at the
|
|
21
|
+
# ends: a short list is quicker to get around when its ends join, and every
|
|
22
|
+
# console menu does it.
|
|
23
|
+
#
|
|
24
|
+
# **The other pair belongs to the focused button.** Only the button knows
|
|
25
|
+
# whether it has anything to change, so it is asked to `adjust` and a plain
|
|
26
|
+
# PanelButton answers nil. That is what makes UI::OptionButton work, and the
|
|
27
|
+
# reason an option row wants this navigation rather than UI::Pointing,
|
|
28
|
+
# where every direction is one to point in.
|
|
29
|
+
class Stepping < Navigation
|
|
30
|
+
ACTIONS = {
|
|
31
|
+
vertical: %i[ui_up ui_down ui_left ui_right].freeze,
|
|
32
|
+
horizontal: %i[ui_left ui_right ui_up ui_down].freeze
|
|
33
|
+
}.freeze
|
|
34
|
+
|
|
35
|
+
# The axis focus moves along: what was passed, or once the menu is
|
|
36
|
+
# built, its layout's. nil before then when none was passed.
|
|
37
|
+
attr_reader :axis
|
|
38
|
+
|
|
39
|
+
def initialize(axis: nil)
|
|
40
|
+
super()
|
|
41
|
+
@axis = axis
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Resolves the axis against the menu's layout. Raises ArgumentError for
|
|
45
|
+
# an axis outside UI::Stack::AXES.
|
|
46
|
+
def attach(menu)
|
|
47
|
+
super
|
|
48
|
+
@axis ||= menu.layout.axis
|
|
49
|
+
unless Stack::AXES.include?(@axis)
|
|
50
|
+
raise ArgumentError, "axis: must be one of #{Stack::AXES.inspect}, not #{@axis.inspect}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
@step_back, @step_on, @adjust_down, @adjust_up = ACTIONS.fetch(@axis)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Moves focus by `delta`, skipping anything disabled, and wrapping. Does
|
|
57
|
+
# nothing at all if no button can take focus.
|
|
58
|
+
def step(delta)
|
|
59
|
+
buttons = menu.buttons
|
|
60
|
+
count = buttons.size
|
|
61
|
+
index = menu.focused_index || 0
|
|
62
|
+
tried = 0
|
|
63
|
+
while tried < count
|
|
64
|
+
index = (index + delta) % count
|
|
65
|
+
if buttons[index].enabled?
|
|
66
|
+
menu.focus(index)
|
|
67
|
+
return
|
|
68
|
+
end
|
|
69
|
+
tried += 1
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def on_control(actions)
|
|
74
|
+
step(-1) if actions.pressed?(@step_back)
|
|
75
|
+
step(1) if actions.pressed?(@step_on)
|
|
76
|
+
|
|
77
|
+
current = menu.focused
|
|
78
|
+
return if current.nil?
|
|
79
|
+
|
|
80
|
+
current.adjust(-1) if actions.pressed?(@adjust_down)
|
|
81
|
+
current.adjust(1) if actions.pressed?(@adjust_up)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def on_buttons_changed
|
|
85
|
+
return if menu.focused&.enabled?
|
|
86
|
+
|
|
87
|
+
first = menu.buttons.index(&:enabled?)
|
|
88
|
+
menu.focus(first || 0)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RGame
|
|
4
|
+
module Engine
|
|
5
|
+
module UI
|
|
6
|
+
# A label centred on a style: the button to build a menu with before any
|
|
7
|
+
# art exists, and the base of every shipped button that draws a label.
|
|
8
|
+
#
|
|
9
|
+
# menu.add(UI::TextButton.new(label: 'play')).on_activated { start }
|
|
10
|
+
# menu.add(UI::TextButton.new(label: 'credits', style: UI::ShapeStyle.new(outline: nil)))
|
|
11
|
+
# menu.add(UI::TextButton.new(label: 'quit', style: nil))
|
|
12
|
+
#
|
|
13
|
+
# With no `style:` it draws UI::ShapeStyle::DEFAULT, which needs nothing
|
|
14
|
+
# registered with the renderer. `style: nil` draws the label alone. The
|
|
15
|
+
# style draws at `z: 0` or below and the label at `z: 1`, so the label is
|
|
16
|
+
# always on top. Focus, pressing and activation are all UI::Button's.
|
|
17
|
+
#
|
|
18
|
+
# The label colours are coerced once, so they may be arrays and a draw
|
|
19
|
+
# still allocates nothing. A style answering `content_color(state)` —
|
|
20
|
+
# UI::ShapeStyle does — overrides them in any state it names, because what
|
|
21
|
+
# reads on a fill is the style's to say: a light label on a gold pressed
|
|
22
|
+
# fill would all but disappear. A subclass that draws more than a label —
|
|
23
|
+
# UI::OptionButton — overrides `draw_foreground`, and so keeps its style
|
|
24
|
+
# without having to remember to draw it.
|
|
25
|
+
class TextButton < Button
|
|
26
|
+
LABEL_COLOR = Util::Color.new(240, 236, 224)
|
|
27
|
+
DISABLED_LABEL_COLOR = Util::Color.new(120, 116, 128)
|
|
28
|
+
|
|
29
|
+
attr_reader :style, :label_color, :disabled_label_color
|
|
30
|
+
|
|
31
|
+
def initialize(label:, style: ShapeStyle::DEFAULT, label_color: LABEL_COLOR,
|
|
32
|
+
disabled_label_color: DISABLED_LABEL_COLOR, **)
|
|
33
|
+
super(label: label, **)
|
|
34
|
+
@style = style
|
|
35
|
+
@style_names_content = style.respond_to?(:content_color)
|
|
36
|
+
@label_color = Util::Color.coerce(label_color)
|
|
37
|
+
@disabled_label_color = Util::Color.coerce(disabled_label_color)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def on_draw(renderer, _view)
|
|
41
|
+
@style&.draw(renderer, state, width, height)
|
|
42
|
+
draw_foreground(renderer)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def draw_foreground(renderer)
|
|
48
|
+
text = @label.to_s
|
|
49
|
+
renderer.text(text, centred_x(renderer, text), label_y(renderer), z: 1, color: current_label_color)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def current_label_color = style_content_color || (@enabled ? @label_color : @disabled_label_color)
|
|
53
|
+
def style_content_color = @style_names_content ? @style.content_color(state) : nil
|
|
54
|
+
def centred_x(renderer, text) = (width - renderer.text_width(text)) / 2
|
|
55
|
+
def label_y(renderer) = (height - renderer.text_height) / 2
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|