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
data/lib/rgame/engine/timer.rb
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
module RGame
|
|
4
4
|
module Engine
|
|
5
5
|
# A repeating interval timer for periodic events that aren't driven by input — a
|
|
6
|
-
# spawner emitting an enemy every N seconds, a
|
|
6
|
+
# spawner emitting an enemy every N seconds, a turret's fire rate, a wave clock.
|
|
7
7
|
#
|
|
8
8
|
# It only accumulates time; the owner decides what each elapsed interval means. That
|
|
9
9
|
# split lets one primitive serve both styles: "act automatically" (consume every ready
|
|
10
10
|
# interval) and "stay loaded until conditions allow" (check `ready?`, but `consume`
|
|
11
|
-
# only when actually acting — so a
|
|
11
|
+
# only when actually acting — so a turret with no target keeps its shot ready instead of
|
|
12
12
|
# wasting it). Pure and allocation-free, so it ticks on the per-frame path.
|
|
13
13
|
#
|
|
14
14
|
# timer = Engine::Timer.new(0.8)
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RGame
|
|
4
|
+
module Engine
|
|
5
|
+
module UI
|
|
6
|
+
# Anything a Menu can hold: the state a button has — enabled, focused,
|
|
7
|
+
# pressed — and a signal for when it is chosen, with no look at all.
|
|
8
|
+
#
|
|
9
|
+
# class Swatch < UI::Button
|
|
10
|
+
# def initialize(color:, **)
|
|
11
|
+
# super(**)
|
|
12
|
+
# @color = color
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# def on_draw(renderer, _view)
|
|
16
|
+
# inset = state == :focused ? 0 : 4
|
|
17
|
+
# renderer.rect(inset, inset, width - (inset * 2), height - (inset * 2), color: @color)
|
|
18
|
+
# end
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# The look is a subclass's `on_draw`, and it reads `state` to decide what
|
|
22
|
+
# to draw. Everything the menu needs from a button — being told it is
|
|
23
|
+
# focused or pressed, being activated, being asked to `adjust` — lives
|
|
24
|
+
# here, so a subclass that only draws cannot leave any of it out.
|
|
25
|
+
#
|
|
26
|
+
# There is no hover, because there is no pointer: what a mouse-driven
|
|
27
|
+
# control would get from the cursor being over it, this gets from the Menu
|
|
28
|
+
# telling it it is the focused one.
|
|
29
|
+
#
|
|
30
|
+
# Its position is its own, resolved through the tree like any node's, so a
|
|
31
|
+
# Menu inside a PlayerLayer puts its buttons inside that player's region
|
|
32
|
+
# without either of them arranging it.
|
|
33
|
+
#
|
|
34
|
+
# A Button with no `on_draw` of its own draws nothing.
|
|
35
|
+
#
|
|
36
|
+
# ## The label is a translation key
|
|
37
|
+
#
|
|
38
|
+
# `label: 'play'` is a key, and the button holds it as an Engine::Text, so
|
|
39
|
+
# what is drawn follows `I18n.locale` without the button being rebuilt.
|
|
40
|
+
# Text that must not be translated — a player's name — is passed as one:
|
|
41
|
+
#
|
|
42
|
+
# UI::TextButton.new(label: 'play') # the key 'play'
|
|
43
|
+
# UI::TextButton.new(label: Engine::Text.literal(name)) # the name, in every language
|
|
44
|
+
#
|
|
45
|
+
# A label is drawn as it reads, so a `Text` with variables shows the values
|
|
46
|
+
# its last `with` was given. The node that owns them calls `with` in
|
|
47
|
+
# `update`, and the label follows:
|
|
48
|
+
#
|
|
49
|
+
# @continue = Engine::Text.new('continue', :saves)
|
|
50
|
+
# menu.add(UI::PanelButton.new(label: @continue))
|
|
51
|
+
# def on_update(_dt) = @continue.with(saves: @save_count)
|
|
52
|
+
#
|
|
53
|
+
# One given no `with` yet raises on the first draw, naming the keywords.
|
|
54
|
+
#
|
|
55
|
+
# `label_scope` puts a scope in front of a label given as a key, and
|
|
56
|
+
# UI::Menu sets it from its own `scope:` as the button is added. A label
|
|
57
|
+
# given as a `Text` is the caller's, scope and all, and nothing changes it.
|
|
58
|
+
#
|
|
59
|
+
# ## When a press activates
|
|
60
|
+
#
|
|
61
|
+
# `activate_on:` is `:release` by default: confirm draws the button pressed
|
|
62
|
+
# while it is held and activates it when it is let go, so a player who
|
|
63
|
+
# moves focus away while still holding cancels. `:press` activates on the
|
|
64
|
+
# way down — what a skill bar wants — and keeps the button drawn pressed
|
|
65
|
+
# for at least `PRESS_FEEDBACK` seconds, because a tap is otherwise a
|
|
66
|
+
# single frame of pressed that nobody sees. Its 0.1 s is Unity's, which
|
|
67
|
+
# holds a submitted button pressed that long; Godot holds a shortcut's for
|
|
68
|
+
# 0.2 s.
|
|
69
|
+
#
|
|
70
|
+
# Either way a button only acts on a press it saw start. A press whose
|
|
71
|
+
# release it did not see — its menu was paused or covered in between — is
|
|
72
|
+
# dropped rather than finished, so a menu that closes itself from
|
|
73
|
+
# `on_activated` and is opened again does not come back pressed. See
|
|
74
|
+
# UI::Menu for the other half, a menu that has not yet seen confirm up.
|
|
75
|
+
#
|
|
76
|
+
# ## A hotkey
|
|
77
|
+
#
|
|
78
|
+
# `hotkey:` names an action that presses this button whether or not it is
|
|
79
|
+
# focused, and without moving focus — a skill bar's number keys:
|
|
80
|
+
#
|
|
81
|
+
# UI::IconButton.new(image: :torch, hotkey: :skill3)
|
|
82
|
+
#
|
|
83
|
+
# A hotkey always activates on the press, whatever `activate_on:` says,
|
|
84
|
+
# and draws pressed for `PRESS_FEEDBACK` or while held. Its release
|
|
85
|
+
# activates nothing. The menu reads it, so the action has to be declared
|
|
86
|
+
# in the player's InputMap; an undeclared one raises on the first frame.
|
|
87
|
+
#
|
|
88
|
+
# **A press belongs to the source that started it.** The menu passes
|
|
89
|
+
# `:confirm` or `:hotkey` to `press`, `release` and `cancel_press`, and
|
|
90
|
+
# while one source holds the button a press from the other is ignored and
|
|
91
|
+
# its release ends nothing. So a player holding confirm on a `:release`
|
|
92
|
+
# button who taps that button's hotkey activates it once, when confirm is
|
|
93
|
+
# let go; and losing focus ends a confirm hold but not a hotkey one.
|
|
94
|
+
class Button < Node2D
|
|
95
|
+
signal :on_activated
|
|
96
|
+
|
|
97
|
+
PRESS_FEEDBACK = 0.1
|
|
98
|
+
STATES = %i[idle focused pressed disabled].freeze
|
|
99
|
+
ACTIVATE_ON = %i[release press].freeze
|
|
100
|
+
SOURCES = %i[confirm hotkey].freeze
|
|
101
|
+
|
|
102
|
+
attr_accessor :enabled
|
|
103
|
+
# The Engine::Text drawn for this button, or nil.
|
|
104
|
+
attr_reader :label
|
|
105
|
+
# The scope a label given as a key resolves under, or nil.
|
|
106
|
+
attr_reader :label_scope
|
|
107
|
+
# `hotkey` is an action name, or nil.
|
|
108
|
+
attr_reader :activate_on, :hotkey
|
|
109
|
+
|
|
110
|
+
def initialize(label: nil, enabled: true, activate_on: :release, hotkey: nil, **)
|
|
111
|
+
super(**)
|
|
112
|
+
unless ACTIVATE_ON.include?(activate_on)
|
|
113
|
+
raise ArgumentError, "activate_on: must be :release or :press, not #{activate_on.inspect}"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
@label_scope = nil
|
|
117
|
+
self.label = label
|
|
118
|
+
@enabled = enabled
|
|
119
|
+
@activate_on = activate_on
|
|
120
|
+
@hotkey = hotkey
|
|
121
|
+
@focused = false
|
|
122
|
+
@holder = nil
|
|
123
|
+
@feedback = 0.0
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Sets the label: a key String or Symbol, which the button makes an
|
|
127
|
+
# Engine::Text of under `label_scope`; a `Text`, used as it is; or nil.
|
|
128
|
+
def label=(label)
|
|
129
|
+
@label = label && text_for(label)
|
|
130
|
+
@label_from_key = !(label.nil? || label.is_a?(Text))
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Sets the scope a label given as a key resolves under — `'title_menu'`
|
|
134
|
+
# makes the key `'play'` read `'title_menu.play'` — and resolves it again
|
|
135
|
+
# on the next draw.
|
|
136
|
+
def label_scope=(scope)
|
|
137
|
+
@label_scope = scope&.to_s
|
|
138
|
+
@label.scope = @label_scope if @label_from_key
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def enabled? = @enabled
|
|
142
|
+
def focused? = @focused
|
|
143
|
+
def pressed? = !@holder.nil? || @feedback.positive?
|
|
144
|
+
|
|
145
|
+
# What to draw: `:disabled` whenever the button is disabled, whatever
|
|
146
|
+
# else is true; then `:pressed`, `:focused`, and otherwise `:idle`.
|
|
147
|
+
# Pressed does not require focus: a press can outlast the focus that
|
|
148
|
+
# started it by its `PRESS_FEEDBACK`.
|
|
149
|
+
def state
|
|
150
|
+
return :disabled unless @enabled
|
|
151
|
+
return :pressed if pressed?
|
|
152
|
+
|
|
153
|
+
@focused ? :focused : :idle
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Called by the Menu. Calls `on_focus_changed` when the value actually
|
|
157
|
+
# changes, and never for a repeated assignment, so a menu that reasserts
|
|
158
|
+
# focus every frame does not replay a focus sound every frame. Losing
|
|
159
|
+
# focus lets go of a confirm press without activating it; a hotkey press
|
|
160
|
+
# was never about focus, and is kept.
|
|
161
|
+
def focused=(value)
|
|
162
|
+
return if @focused == value
|
|
163
|
+
|
|
164
|
+
@focused = value
|
|
165
|
+
@holder = nil if !value && @holder == :confirm
|
|
166
|
+
on_focus_changed(value)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Called by the Menu when a press from `source` — `:confirm` or
|
|
170
|
+
# `:hotkey` — starts on this button. A hotkey press, or any press under
|
|
171
|
+
# `activate_on: :press`, activates at once and returns what `activate`
|
|
172
|
+
# did; a confirm press under `:release` holds and returns nil. Ignored,
|
|
173
|
+
# returning nil, while disabled or while any source already holds it.
|
|
174
|
+
def press(source = :confirm)
|
|
175
|
+
unless SOURCES.include?(source)
|
|
176
|
+
raise ArgumentError, "source must be :confirm or :hotkey, not #{source.inspect}"
|
|
177
|
+
end
|
|
178
|
+
return nil unless @enabled && @holder.nil?
|
|
179
|
+
|
|
180
|
+
@holder = source
|
|
181
|
+
return nil unless source == :hotkey || @activate_on == :press
|
|
182
|
+
|
|
183
|
+
activate_with_feedback
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Called by the Menu when a press `source` started is let go. Activates
|
|
187
|
+
# a confirm press under `activate_on: :release`, and returns what
|
|
188
|
+
# `activate` did; ends nothing another source started.
|
|
189
|
+
def release(source = :confirm)
|
|
190
|
+
return nil unless @holder == source
|
|
191
|
+
|
|
192
|
+
@holder = nil
|
|
193
|
+
activate if source == :confirm && @activate_on == :release
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Called by the Menu for a press from `source` whose release this button
|
|
197
|
+
# never saw. Drops it, and the feedback with it, activating nothing.
|
|
198
|
+
def cancel_press(source = :confirm)
|
|
199
|
+
return unless @holder == source
|
|
200
|
+
|
|
201
|
+
@holder = nil
|
|
202
|
+
@feedback = 0.0
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# The instant press: activates, and shows pressed for `PRESS_FEEDBACK`
|
|
206
|
+
# though nothing holds the button. What a hotkey press and a
|
|
207
|
+
# `:press` confirm do, available on its own for a press that ends
|
|
208
|
+
# somewhere other than on this button. Returns what `activate` did.
|
|
209
|
+
def activate_with_feedback
|
|
210
|
+
return nil unless @enabled
|
|
211
|
+
|
|
212
|
+
@feedback = PRESS_FEEDBACK
|
|
213
|
+
activate
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# What the focused button does with `ui_left` / `ui_right`, which the
|
|
217
|
+
# menu hands down without knowing what kind of button it is talking to.
|
|
218
|
+
# A plain button has nothing to change, and answers nil the way a
|
|
219
|
+
# disabled `activate` does. UI::OptionButton is the one that overrides it.
|
|
220
|
+
def adjust(_delta) = nil
|
|
221
|
+
|
|
222
|
+
# Fires the signal and returns the button, or nil if it is disabled — so
|
|
223
|
+
# a caller never has to check first, and a disabled button cannot be
|
|
224
|
+
# activated by any route.
|
|
225
|
+
def activate
|
|
226
|
+
return nil unless @enabled
|
|
227
|
+
|
|
228
|
+
on_activated_signal.emit
|
|
229
|
+
self
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Counts down the pressed feedback, then does what every node does.
|
|
233
|
+
def update(dt)
|
|
234
|
+
@feedback -= dt if @feedback.positive? && !@paused
|
|
235
|
+
super
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Hook: override to react to gaining or losing focus — a sound, the
|
|
239
|
+
# start of an animation. Called only on a change.
|
|
240
|
+
def on_focus_changed(focused); end
|
|
241
|
+
|
|
242
|
+
private
|
|
243
|
+
|
|
244
|
+
def text_for(shown) = shown.is_a?(Text) ? shown : Text.new(shown, scope: @label_scope)
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RGame
|
|
4
|
+
module Engine
|
|
5
|
+
module UI
|
|
6
|
+
# A Menu layout: buttons stacked downwards from the menu's origin, all the
|
|
7
|
+
# same size, `spacing` pixels apart. A UI::Stack on the vertical axis, so
|
|
8
|
+
# UI::Stepping moves through it with `ui_up` and `ui_down`.
|
|
9
|
+
#
|
|
10
|
+
# UI::Menu.new(layout: UI::Column.new(item_width: 220, item_height: 44))
|
|
11
|
+
#
|
|
12
|
+
# It takes no `axis:` — a column that is not vertical is a UI::Row.
|
|
13
|
+
class Column < Stack
|
|
14
|
+
def initialize(item_width:, item_height:, spacing: 8)
|
|
15
|
+
super(axis: :vertical, item_width: item_width, item_height: item_height, spacing: spacing)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RGame
|
|
4
|
+
module Engine
|
|
5
|
+
module UI
|
|
6
|
+
# A picture, tinted by state, with an optional caption: the round,
|
|
7
|
+
# image-only entry of a quick-select wheel, or a skill with its name
|
|
8
|
+
# underneath.
|
|
9
|
+
#
|
|
10
|
+
# disc = UI::ShapeStyle.new(shape: :disc)
|
|
11
|
+
# wheel.add(UI::IconButton.new(image: :home, style: disc)).on_activated { go_home }
|
|
12
|
+
# bar.add(UI::IconButton.new(image: :torch, label: 'torch', style: disc))
|
|
13
|
+
#
|
|
14
|
+
# `image:` is an image id — a registered Symbol or a path String — drawn at
|
|
15
|
+
# its natural size, centred in the slot, or centred in the space above the
|
|
16
|
+
# caption when there is one. The caption is centred along the bottom edge,
|
|
17
|
+
# inside the slot, so the slot is still the button's whole extent.
|
|
18
|
+
#
|
|
19
|
+
# **A caption sits under the style, not on it.** With a caption the style
|
|
20
|
+
# is handed the space above it, so a disc is drawn round the picture and the
|
|
21
|
+
# caption reads on whatever is behind the button, in the button's own label
|
|
22
|
+
# colours. A caption wider than the disc — "Watering can" under a tool — is
|
|
23
|
+
# the ordinary case in a skill bar, and one drawn across a disc's edge would
|
|
24
|
+
# be half on the fill and half off it, legible on neither.
|
|
25
|
+
#
|
|
26
|
+
# `image: nil` draws no picture and leaves the caption, for an entry whose
|
|
27
|
+
# art is not in yet. An id nobody registered is a mistake rather than that
|
|
28
|
+
# case, and fails on the first draw like any other image.
|
|
29
|
+
#
|
|
30
|
+
# **Tint is a multiply**, so the art should be white: white takes each
|
|
31
|
+
# colour in `tints:` exactly, and dark art takes none of them. Scales
|
|
32
|
+
# default to 1 in every state because images sample nearest-neighbour, so
|
|
33
|
+
# any scale but a whole number doubles some pixel rows and not others; focus
|
|
34
|
+
# shows through the tint and the style instead. `tints:` and `scales:` need
|
|
35
|
+
# every state, checked here rather than the first frame one is reached.
|
|
36
|
+
#
|
|
37
|
+
# No style by default: an icon on its own is a complete look. The style is
|
|
38
|
+
# drawn at `z: 0` or below, the picture and the caption at `z: 1`. A style
|
|
39
|
+
# answering `content_color(state)` — UI::ShapeStyle does — replaces the tint
|
|
40
|
+
# in any state it names, because what reads on its fill is the style's to
|
|
41
|
+
# say: the pressed tint is the same gold as a ShapeStyle's pressed fill. It
|
|
42
|
+
# does not touch the caption, which is not on the fill.
|
|
43
|
+
class IconButton < Button
|
|
44
|
+
TINTS = {
|
|
45
|
+
idle: Util::Color.new(200, 200, 212),
|
|
46
|
+
focused: Util::Color.new(255, 255, 255),
|
|
47
|
+
pressed: Util::Color.new(240, 200, 96),
|
|
48
|
+
disabled: Util::Color.new(90, 88, 100)
|
|
49
|
+
}.freeze
|
|
50
|
+
SCALES = { idle: 1, focused: 1, pressed: 1, disabled: 1 }.freeze
|
|
51
|
+
|
|
52
|
+
attr_reader :image, :style, :tints, :scales, :label_color, :disabled_label_color
|
|
53
|
+
|
|
54
|
+
def initialize(image:, style: nil, tints: TINTS, scales: SCALES,
|
|
55
|
+
label_color: TextButton::LABEL_COLOR,
|
|
56
|
+
disabled_label_color: TextButton::DISABLED_LABEL_COLOR, **)
|
|
57
|
+
super(**)
|
|
58
|
+
@image = image
|
|
59
|
+
@style = style
|
|
60
|
+
@style_names_content = style.respond_to?(:content_color)
|
|
61
|
+
@tints = STATES.to_h { |state| [state, Util::Color.coerce(tints.fetch(state))] }.freeze
|
|
62
|
+
@scales = STATES.to_h { |state| [state, scales.fetch(state)] }.freeze
|
|
63
|
+
@label_color = Util::Color.coerce(label_color)
|
|
64
|
+
@disabled_label_color = Util::Color.coerce(disabled_label_color)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def on_draw(renderer, _view)
|
|
68
|
+
current = state
|
|
69
|
+
caption_height = @label ? renderer.text_height : 0
|
|
70
|
+
@style&.draw(renderer, current, width, height - caption_height)
|
|
71
|
+
content = @style_names_content ? @style.content_color(current) : nil
|
|
72
|
+
draw_image(renderer, current, content, (height - caption_height) / 2.0)
|
|
73
|
+
draw_caption(renderer, caption_height) if @label
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def draw_image(renderer, current, content, cy)
|
|
79
|
+
return unless @image
|
|
80
|
+
|
|
81
|
+
renderer.image(@image, width / 2.0, cy,
|
|
82
|
+
scale: @scales.fetch(current), z: 1, color: content || @tints.fetch(current))
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def draw_caption(renderer, caption_height)
|
|
86
|
+
text = @label.to_s
|
|
87
|
+
renderer.text(text, (width - renderer.text_width(text)) / 2, height - caption_height,
|
|
88
|
+
z: 1, color: @enabled ? @label_color : @disabled_label_color)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|