rgame 0.4.0-x86_64-linux-gnu
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 +7 -0
- data/CHANGELOG.md +251 -0
- data/LICENSE +26 -0
- data/README.md +325 -0
- data/docs/api/README.md +224 -0
- data/docs/api/app.md +249 -0
- data/docs/api/assets.md +463 -0
- data/docs/api/audio.md +249 -0
- data/docs/api/cli.md +259 -0
- data/docs/api/components.md +1112 -0
- data/docs/api/drawing.md +398 -0
- data/docs/api/examples.md +263 -0
- data/docs/api/game.md +219 -0
- data/docs/api/images.md +118 -0
- data/docs/api/input.md +476 -0
- data/docs/api/internals.md +289 -0
- data/docs/api/localization.md +285 -0
- data/docs/api/scene_graph.md +539 -0
- data/docs/api/signals.md +147 -0
- data/docs/api/systems.md +231 -0
- data/docs/api/text.md +133 -0
- data/docs/api/tile_maps.md +237 -0
- data/docs/api/toolbox.md +493 -0
- data/docs/api/ui.md +927 -0
- data/docs/api/values.md +316 -0
- 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/lib/rgame/boot.rb +3 -0
- 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 +46 -0
- data/lib/rgame/core/asset_manager.rb +206 -0
- data/lib/rgame/core/audio.rb +145 -0
- data/lib/rgame/core/font.rb +46 -0
- data/lib/rgame/core/gamepad.rb +55 -0
- data/lib/rgame/core/image.rb +55 -0
- data/lib/rgame/core/input.rb +71 -0
- data/lib/rgame/core/locale.rb +22 -0
- data/lib/rgame/core/nine_slice.rb +142 -0
- data/lib/rgame/core/recording.rb +54 -0
- data/lib/rgame/core/renderer.rb +354 -0
- data/lib/rgame/core/sprite_sheet.rb +105 -0
- data/lib/rgame/core/tile_map_renderer.rb +172 -0
- data/lib/rgame/core/ui_atlas.rb +101 -0
- data/lib/rgame/core/virtual_gamepad.rb +26 -0
- data/lib/rgame/core.rb +18 -0
- data/lib/rgame/core_ext.so +0 -0
- data/lib/rgame/engine/actor_blockers.rb +131 -0
- data/lib/rgame/engine/animation_set.rb +50 -0
- data/lib/rgame/engine/animator.rb +44 -0
- data/lib/rgame/engine/audio_bus.rb +24 -0
- data/lib/rgame/engine/audio_director.rb +59 -0
- data/lib/rgame/engine/bounds_blockers.rb +74 -0
- data/lib/rgame/engine/camera.rb +78 -0
- data/lib/rgame/engine/circle_collider.rb +34 -0
- data/lib/rgame/engine/collision_box.rb +59 -0
- data/lib/rgame/engine/collision_system.rb +132 -0
- data/lib/rgame/engine/component.rb +64 -0
- data/lib/rgame/engine/components/action_trigger.rb +40 -0
- data/lib/rgame/engine/components/animated_sprite.rb +71 -0
- data/lib/rgame/engine/components/box_collider.rb +99 -0
- data/lib/rgame/engine/components/camera_follow.rb +45 -0
- data/lib/rgame/engine/components/character_body.rb +50 -0
- data/lib/rgame/engine/components/circle_collider.rb +80 -0
- data/lib/rgame/engine/components/collision_world.rb +231 -0
- data/lib/rgame/engine/components/despawn_offscreen.rb +42 -0
- 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 +176 -0
- data/lib/rgame/engine/components/player_controller.rb +27 -0
- data/lib/rgame/engine/components/pool.rb +53 -0
- data/lib/rgame/engine/components/screen_wrap.rb +49 -0
- data/lib/rgame/engine/components/sprite.rb +47 -0
- data/lib/rgame/engine/components/targeting.rb +52 -0
- data/lib/rgame/engine/components/thrust_controller.rb +65 -0
- data/lib/rgame/engine/components/tile_world.rb +109 -0
- data/lib/rgame/engine/components/timer.rb +75 -0
- data/lib/rgame/engine/components/velocity.rb +43 -0
- data/lib/rgame/engine/components/wander_controller.rb +64 -0
- data/lib/rgame/engine/components/world.rb +133 -0
- data/lib/rgame/engine/contact_set.rb +74 -0
- data/lib/rgame/engine/culling.rb +45 -0
- data/lib/rgame/engine/debug_overlay.rb +111 -0
- 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 +322 -0
- data/lib/rgame/engine/input/action_mapper.rb +100 -0
- data/lib/rgame/engine/input/actions.rb +92 -0
- data/lib/rgame/engine/input/input_map.rb +196 -0
- data/lib/rgame/engine/layout.rb +82 -0
- data/lib/rgame/engine/nav_grid.rb +87 -0
- data/lib/rgame/engine/node2d.rb +555 -0
- data/lib/rgame/engine/path.rb +76 -0
- data/lib/rgame/engine/player.rb +69 -0
- data/lib/rgame/engine/player_layer.rb +70 -0
- data/lib/rgame/engine/players.rb +205 -0
- data/lib/rgame/engine/pool.rb +51 -0
- data/lib/rgame/engine/presentation.rb +171 -0
- data/lib/rgame/engine/scene/scene_stack.rb +87 -0
- data/lib/rgame/engine/sealed_privates.rb +54 -0
- data/lib/rgame/engine/signal.rb +75 -0
- data/lib/rgame/engine/spatial_hash.rb +116 -0
- data/lib/rgame/engine/text.rb +194 -0
- data/lib/rgame/engine/tile_blockers.rb +63 -0
- data/lib/rgame/engine/tile_map.rb +147 -0
- data/lib/rgame/engine/tile_map_layer.rb +82 -0
- data/lib/rgame/engine/tileset.rb +99 -0
- data/lib/rgame/engine/timer.rb +51 -0
- 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 +290 -0
- 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/view.rb +76 -0
- data/lib/rgame/engine/viewports.rb +171 -0
- data/lib/rgame/engine/world_view.rb +71 -0
- data/lib/rgame/engine.rb +86 -0
- data/lib/rgame/fonts/LiberationSans-Regular.ttf +0 -0
- data/lib/rgame/fonts/OFL.txt +102 -0
- data/lib/rgame/game.rb +271 -0
- 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 +23 -0
- data/lib/rgame/util/controls.rb +169 -0
- 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 +3 -0
- data/lib/rgame/util/tile_sweep.rb +36 -0
- data/lib/rgame/util/z.rb +123 -0
- data/lib/rgame/util.rb +10 -0
- data/lib/rgame/util_ext.so +0 -0
- data/lib/rgame/version.rb +12 -0
- data/lib/rgame.rb +5 -0
- data/licenses/SDL2/LICENSE.txt +18 -0
- metadata +309 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RGame
|
|
4
|
+
module Engine
|
|
5
|
+
# What physical inputs mean, for one player.
|
|
6
|
+
#
|
|
7
|
+
# map = InputMap.new(
|
|
8
|
+
# thrust: { axis: [Controls::KEY_DOWN, Controls::KEY_UP], stick: Controls::AXIS_TRIGGER_RIGHT },
|
|
9
|
+
# fire: { buttons: [Controls::KEY_SPACE, Controls::PAD_A] }
|
|
10
|
+
# )
|
|
11
|
+
#
|
|
12
|
+
# One entry per action, naming **physical ids from RGame::Util::Controls**
|
|
13
|
+
# directly. That is the whole point of this class: it is the single table a
|
|
14
|
+
# rebinding screen edits, and it holds nothing but integers, so the engine
|
|
15
|
+
# layer may own one outright.
|
|
16
|
+
#
|
|
17
|
+
# Three kinds of source, and an action may combine them:
|
|
18
|
+
#
|
|
19
|
+
# | Key | Reads as | Meaning |
|
|
20
|
+
# |---|---|---|
|
|
21
|
+
# | `buttons:` | `held?` | down if *any* listed id is down |
|
|
22
|
+
# | `axis:` | `axis` | `[negative_id, positive_id]`, or a list of such pairs — a digital axis from buttons |
|
|
23
|
+
# | `stick:` | `axis` | an analog axis id, for a real stick or trigger |
|
|
24
|
+
#
|
|
25
|
+
# ## One table serves every device
|
|
26
|
+
#
|
|
27
|
+
# Listing a key and a pad button in the same entry is safe, and needs no
|
|
28
|
+
# per-device branching, because **a device only answers for its own kind of
|
|
29
|
+
# input** — asking a gamepad about a keyboard scancode is `false`, never the
|
|
30
|
+
# keyboard's answer (see docs/api/input.md). So `fire` can be "Space or A"
|
|
31
|
+
# and each player's device picks out the half that applies to it.
|
|
32
|
+
#
|
|
33
|
+
# ## An axis can have several pairs, like a button can have several ids
|
|
34
|
+
#
|
|
35
|
+
# `axis: [KEY_LEFT, KEY_RIGHT]` is the common case and stays a bare pair.
|
|
36
|
+
# A list of pairs binds more than one control to the same axis:
|
|
37
|
+
#
|
|
38
|
+
# move_x: { axis: [[Controls::KEY_LEFT, Controls::KEY_RIGHT],
|
|
39
|
+
# [Controls::PAD_DPAD_LEFT, Controls::PAD_DPAD_RIGHT]],
|
|
40
|
+
# stick: Controls::AXIS_LEFT_X }
|
|
41
|
+
#
|
|
42
|
+
# Without it a d-pad cannot drive movement at all, because the same action
|
|
43
|
+
# already needed the arrow keys. The largest deflection wins, so the pairs
|
|
44
|
+
# cost nothing on a device that has only one of them.
|
|
45
|
+
#
|
|
46
|
+
# This replaces a two-stage scheme in which a game's action map named
|
|
47
|
+
# RGame::Core::Input's action names, which named physical ids — two tables in
|
|
48
|
+
# series, neither of them the one a config screen wanted, and the lower one
|
|
49
|
+
# unreachable from the engine layer, which may not name Core at all.
|
|
50
|
+
#
|
|
51
|
+
# ## A stick's sign is the device's, not the game's
|
|
52
|
+
#
|
|
53
|
+
# `AXIS_LEFT_Y` is positive *downwards*, like screen coordinates. An action
|
|
54
|
+
# that wants the opposite ("thrust", "climb") negates at the call site or
|
|
55
|
+
# binds a trigger instead — the map stays declarative rather than growing an
|
|
56
|
+
# inversion flag that every reader would then have to check for.
|
|
57
|
+
class InputMap
|
|
58
|
+
Controls = RGame::Util::Controls
|
|
59
|
+
|
|
60
|
+
# One action's resolved sources. Built once, at construction, so polling
|
|
61
|
+
# walks plain attribute reads and allocates nothing.
|
|
62
|
+
#
|
|
63
|
+
# `buttons` is "held if any of these is down"; `pairs` is a list of
|
|
64
|
+
# `[negative, positive]` button pairs, each a digital axis; `stick` is an
|
|
65
|
+
# analog axis id.
|
|
66
|
+
Binding = Struct.new(:buttons, :pairs, :stick)
|
|
67
|
+
|
|
68
|
+
SOURCES = %i[buttons axis stick].freeze
|
|
69
|
+
|
|
70
|
+
UI = {
|
|
71
|
+
ui_up: { buttons: [Controls::KEY_UP, Controls::PAD_DPAD_UP] },
|
|
72
|
+
ui_down: { buttons: [Controls::KEY_DOWN, Controls::PAD_DPAD_DOWN] },
|
|
73
|
+
ui_left: { buttons: [Controls::KEY_LEFT, Controls::PAD_DPAD_LEFT] },
|
|
74
|
+
ui_right: { buttons: [Controls::KEY_RIGHT, Controls::PAD_DPAD_RIGHT] },
|
|
75
|
+
ui_confirm: { buttons: [Controls::KEY_RETURN, Controls::KEY_SPACE, Controls::PAD_A] },
|
|
76
|
+
ui_cancel: { buttons: [Controls::KEY_ESCAPE, Controls::PAD_B] },
|
|
77
|
+
ui_radial_x: { axis: [[Controls::KEY_LEFT, Controls::KEY_RIGHT],
|
|
78
|
+
[Controls::PAD_DPAD_LEFT, Controls::PAD_DPAD_RIGHT]],
|
|
79
|
+
stick: Controls::AXIS_LEFT_X },
|
|
80
|
+
ui_radial_y: { axis: [[Controls::KEY_UP, Controls::KEY_DOWN],
|
|
81
|
+
[Controls::PAD_DPAD_UP, Controls::PAD_DPAD_DOWN]],
|
|
82
|
+
stick: Controls::AXIS_LEFT_Y }
|
|
83
|
+
}.freeze
|
|
84
|
+
|
|
85
|
+
DEFAULT_ACTIONS = {
|
|
86
|
+
move_x: { axis: [[Controls::KEY_LEFT, Controls::KEY_RIGHT],
|
|
87
|
+
[Controls::KEY_A, Controls::KEY_D],
|
|
88
|
+
[Controls::PAD_DPAD_LEFT, Controls::PAD_DPAD_RIGHT]],
|
|
89
|
+
stick: Controls::AXIS_LEFT_X },
|
|
90
|
+
move_y: { axis: [[Controls::KEY_UP, Controls::KEY_DOWN],
|
|
91
|
+
[Controls::KEY_W, Controls::KEY_S],
|
|
92
|
+
[Controls::PAD_DPAD_UP, Controls::PAD_DPAD_DOWN]],
|
|
93
|
+
stick: Controls::AXIS_LEFT_Y },
|
|
94
|
+
fire: { buttons: [Controls::KEY_SPACE, Controls::PAD_A] }
|
|
95
|
+
}.freeze
|
|
96
|
+
|
|
97
|
+
# Every action this map can answer for, as `name => Binding`.
|
|
98
|
+
attr_reader :bindings
|
|
99
|
+
|
|
100
|
+
# `entries` are merged over the universal UI set, so declaring a game's
|
|
101
|
+
# actions never costs it the ones the UI needs.
|
|
102
|
+
def initialize(entries = {})
|
|
103
|
+
@bindings = UI.merge(entries).to_h { |name, entry| [name, build(name, entry)] }.freeze
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# The default map: the UI set plus DEFAULT_ACTIONS.
|
|
107
|
+
def self.default = new(DEFAULT_ACTIONS)
|
|
108
|
+
|
|
109
|
+
# A copy with `entries` overriding, which is how a game rebinds one action
|
|
110
|
+
# without restating the rest.
|
|
111
|
+
def merge(entries) = self.class.new(to_h.merge(entries))
|
|
112
|
+
|
|
113
|
+
def [](action) = @bindings[action]
|
|
114
|
+
|
|
115
|
+
def actions = @bindings.keys
|
|
116
|
+
|
|
117
|
+
# The button bound to `action` that `device` can actually press, or nil.
|
|
118
|
+
#
|
|
119
|
+
# This undoes what the section above describes. One entry lists a key and
|
|
120
|
+
# a pad button together so that polling needs no branch, and that is right
|
|
121
|
+
# for reading input and wrong for *showing* it: a prompt has to say "press
|
|
122
|
+
# A" or "press Space", never both, and which one depends on what the
|
|
123
|
+
# player last touched.
|
|
124
|
+
#
|
|
125
|
+
# map.button_for(:fire, Controls::KEYBOARD) # => KEY_SPACE
|
|
126
|
+
# map.button_for(:fire, Controls.gamepad(0)) # => PAD_A
|
|
127
|
+
#
|
|
128
|
+
# The first match wins, so the order an entry lists its ids in is the order
|
|
129
|
+
# a prompt prefers them — `ui_confirm` names Return before Space, and a
|
|
130
|
+
# prompt for it says Return.
|
|
131
|
+
#
|
|
132
|
+
# Nil for an action nobody bound, for one with no buttons at all (a stick
|
|
133
|
+
# or a digital axis is not a button and a prompt for one is a different
|
|
134
|
+
# picture), and for a device kind the entry does not cover. A caller
|
|
135
|
+
# showing a prompt has to handle that nil either way, because a rebinding
|
|
136
|
+
# screen can leave an action unbound.
|
|
137
|
+
#
|
|
138
|
+
# Allocation-free, so a HUD may call it per frame rather than caching a
|
|
139
|
+
# string it would then have to invalidate.
|
|
140
|
+
def button_for(action, device)
|
|
141
|
+
binding = @bindings[action]
|
|
142
|
+
buttons = binding&.buttons
|
|
143
|
+
return nil if buttons.nil?
|
|
144
|
+
|
|
145
|
+
pad = Controls.gamepad?(device)
|
|
146
|
+
buttons.find { |id| Controls.pad_button?(id) == pad }
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# The entries in the shape they were declared in, so a map can be edited
|
|
150
|
+
# and rebuilt (a config screen) or merged.
|
|
151
|
+
def to_h
|
|
152
|
+
@bindings.to_h do |name, binding|
|
|
153
|
+
entry = {}
|
|
154
|
+
entry[:buttons] = binding.buttons if binding.buttons
|
|
155
|
+
entry[:axis] = binding.pairs.size == 1 ? binding.pairs.first : binding.pairs if binding.pairs
|
|
156
|
+
entry[:stick] = binding.stick if binding.stick
|
|
157
|
+
[name, entry]
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
private
|
|
162
|
+
|
|
163
|
+
def build(name, entry)
|
|
164
|
+
unknown = entry.keys - SOURCES
|
|
165
|
+
raise ArgumentError, "#{name}: unknown source #{unknown.first.inspect}" unless unknown.empty?
|
|
166
|
+
|
|
167
|
+
buttons = freeze_ids(name, entry[:buttons])
|
|
168
|
+
pairs = axis_pairs(name, entry[:axis])
|
|
169
|
+
raise ArgumentError, "#{name}: no buttons, axis or stick" if buttons.nil? && pairs.nil? && entry[:stick].nil?
|
|
170
|
+
|
|
171
|
+
Binding.new(buttons, pairs, entry[:stick]).freeze
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def freeze_ids(name, ids)
|
|
175
|
+
return nil if ids.nil?
|
|
176
|
+
raise ArgumentError, "#{name}: buttons must be a list of ids" unless ids.is_a?(Array) && !ids.empty?
|
|
177
|
+
|
|
178
|
+
ids.dup.freeze
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def axis_pairs(name, axis)
|
|
182
|
+
return nil if axis.nil?
|
|
183
|
+
|
|
184
|
+
pairs = axis.is_a?(Array) && axis.first.is_a?(Array) ? axis : [axis]
|
|
185
|
+
pairs.each { |pair| check_pair(name, pair) }
|
|
186
|
+
pairs.map { |pair| pair.dup.freeze }.freeze
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def check_pair(name, pair)
|
|
190
|
+
return if pair.is_a?(Array) && pair.size == 2
|
|
191
|
+
|
|
192
|
+
raise ArgumentError, "#{name}: axis must be [negative_id, positive_id], or a list of those"
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RGame
|
|
4
|
+
module Engine
|
|
5
|
+
# Where each viewport goes on screen: pure rectangle arithmetic, and nothing
|
|
6
|
+
# else.
|
|
7
|
+
#
|
|
8
|
+
# Layout.rects(2, 640, 480) # => [[0, 0, 640, 240], [0, 240, 640, 240]]
|
|
9
|
+
#
|
|
10
|
+
# No state, no anchors, no lifetime — so it is specced on its own with no
|
|
11
|
+
# tree and no window, which is the point of separating it from Viewports.
|
|
12
|
+
# That system holds *which* mode is current and *who* is playing; this only
|
|
13
|
+
# answers "given a count and a window, where do they go".
|
|
14
|
+
#
|
|
15
|
+
# ## Rects tile exactly
|
|
16
|
+
#
|
|
17
|
+
# Edges are computed as `(i * total) / count` rather than by multiplying a
|
|
18
|
+
# rounded size, so three rows of a 481-pixel window are 161, 160 and 160 and
|
|
19
|
+
# the last one still ends exactly at 481. Dividing first leaves a seam at the
|
|
20
|
+
# bottom of the screen that nothing draws into — a one-pixel line that looks
|
|
21
|
+
# like a rendering bug and is really a rounding one.
|
|
22
|
+
module Layout
|
|
23
|
+
# A window's worth of rows, columns or cells for `count` viewports.
|
|
24
|
+
#
|
|
25
|
+
# Two players get rows rather than columns because halving the height of a
|
|
26
|
+
# landscape window leaves each view landscape, while halving the width
|
|
27
|
+
# leaves two tall slots that fit a 2D scene badly. Three and four share a
|
|
28
|
+
# 2x2 grid, with the fourth cell left empty for three.
|
|
29
|
+
# Forwards its block with `&block` rather than re-yielding through
|
|
30
|
+
# `{ |*args| yield(*args) }`, which builds an Array per yield. Passing a
|
|
31
|
+
# block straight on allocates nothing — measured, not assumed; see the
|
|
32
|
+
# allocation example in layout_spec.rb.
|
|
33
|
+
def self.each_rect(count, width, height, &)
|
|
34
|
+
return if count <= 0
|
|
35
|
+
return yield(0, 0, 0, width, height) if count == 1
|
|
36
|
+
return each_row(count, width, height, &) if count == 2
|
|
37
|
+
|
|
38
|
+
each_cell(count, 2, 2, width, height, &)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# The same, as an Array of `[x, y, width, height]`. For specs and setup;
|
|
42
|
+
# the per-frame path uses #each_rect, which allocates nothing.
|
|
43
|
+
def self.rects(count, width, height)
|
|
44
|
+
result = []
|
|
45
|
+
each_rect(count, width, height) { |_i, x, y, w, h| result << [x, y, w, h] }
|
|
46
|
+
result
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# `count` full-width rows, stacked.
|
|
50
|
+
def self.each_row(count, width, height)
|
|
51
|
+
count.times do |i|
|
|
52
|
+
top = edge(i, count, height)
|
|
53
|
+
yield(i, 0, top, width, edge(i + 1, count, height) - top)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# `count` full-height columns, side by side.
|
|
58
|
+
def self.each_column(count, width, height)
|
|
59
|
+
count.times do |i|
|
|
60
|
+
left = edge(i, count, width)
|
|
61
|
+
yield(i, left, 0, edge(i + 1, count, width) - left, height)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# The first `count` cells of a `cols` x `rows` grid, filled left to right,
|
|
66
|
+
# top to bottom.
|
|
67
|
+
def self.each_cell(count, cols, rows, width, height)
|
|
68
|
+
count.times do |i|
|
|
69
|
+
col = i % cols
|
|
70
|
+
row = i / cols
|
|
71
|
+
left = edge(col, cols, width)
|
|
72
|
+
top = edge(row, rows, height)
|
|
73
|
+
yield(i, left, top, edge(col + 1, cols, width) - left, edge(row + 1, rows, height) - top)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# The i-th boundary of `count` even divisions of `total`. Multiplying
|
|
78
|
+
# before dividing is what makes the divisions tile with no seam.
|
|
79
|
+
def self.edge(index, count, total) = (index * total) / count
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RGame
|
|
4
|
+
module Engine
|
|
5
|
+
# A walkability grid and the searches over it: region labels and A* routes, run in C by a
|
|
6
|
+
# Util::RouteSearch over a Util::SolidGrid.
|
|
7
|
+
#
|
|
8
|
+
# Where TileBlockers answers *what is in the way of a step*, this answers *what is in the
|
|
9
|
+
# way of a route*: the same fact about the same cells, asked over a graph instead of along
|
|
10
|
+
# a box's sweep. Components::TileWorld owns both, over one store.
|
|
11
|
+
#
|
|
12
|
+
# It is built one of two ways:
|
|
13
|
+
#
|
|
14
|
+
# - **`grid:`** — over a SolidGrid it shares. A cell changed in that grid is changed here
|
|
15
|
+
# at once, and region labels follow it on the next query with nothing rebuilt. This is
|
|
16
|
+
# how a TileWorld builds one, and the way to build one over solidity that will change.
|
|
17
|
+
# - **`width:`, `height:`, `solid:`** — from a callable, `solid.call(col, row) -> bool`,
|
|
18
|
+
# read once per cell into a private grid of its own. Nothing re-reads the callable, so a
|
|
19
|
+
# change behind it is never seen.
|
|
20
|
+
#
|
|
21
|
+
# Anything else — both, or neither — is an ArgumentError.
|
|
22
|
+
#
|
|
23
|
+
# Routes are cells, never pixels. Turning a cell into the point a node must reach depends
|
|
24
|
+
# on the node's collider, which a grid has no business knowing; Components::Navigator
|
|
25
|
+
# does that.
|
|
26
|
+
#
|
|
27
|
+
# The grid is 8-connected with octile costs (1 straight, √2 diagonal), and a diagonal step
|
|
28
|
+
# is allowed only when both orthogonal cells beside it are open, so a route never cuts the
|
|
29
|
+
# corner of a solid cell. Connected regions are labelled whenever the grid has changed
|
|
30
|
+
# since they last were, which makes an unreachable goal — the answer a search is slowest
|
|
31
|
+
# to give, because it has to exhaust the region first — a lookup whenever it lies in
|
|
32
|
+
# another region.
|
|
33
|
+
#
|
|
34
|
+
# Coordinates are Integers, and anything else is a TypeError. An Integer outside the
|
|
35
|
+
# grid, however large, is answered like any cell outside it.
|
|
36
|
+
#
|
|
37
|
+
# A search runs on demand, never per frame, and allocates its result; everything else it
|
|
38
|
+
# keeps is reused between searches. One grid is therefore not safe to search from two
|
|
39
|
+
# threads at once.
|
|
40
|
+
class NavGrid
|
|
41
|
+
def initialize(grid: nil, width: nil, height: nil, solid: nil)
|
|
42
|
+
@grid = grid ? shared(grid, width, height, solid) : private_grid(width, height, solid)
|
|
43
|
+
@search = Util::RouteSearch.new(@grid)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def width = @grid.width
|
|
47
|
+
def height = @grid.height
|
|
48
|
+
|
|
49
|
+
def walkable?(col, row)
|
|
50
|
+
!@grid.solid?(col, row) && col >= 0 && row >= 0 && col < @grid.width && row < @grid.height
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# The connected region containing the cell, as an Integer label — two walkable cells
|
|
54
|
+
# share a label exactly when a route joins them — or nil for a solid cell or one
|
|
55
|
+
# outside the grid.
|
|
56
|
+
def region(col, row) = @search.region(col, row)
|
|
57
|
+
|
|
58
|
+
def reachable?(from_col, from_row, to_col, to_row)
|
|
59
|
+
from = @search.region(from_col, from_row)
|
|
60
|
+
to = @search.region(to_col, to_row)
|
|
61
|
+
!from.nil? && from == to
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# The cheapest route from start to goal, both included, as `[[col, row], ...]` — or
|
|
65
|
+
# nil when either end is solid, outside the grid, or in another region. Start equal
|
|
66
|
+
# to goal is a route of one cell. Ties between equally cheap routes are broken the
|
|
67
|
+
# same way every time, so the same query always returns the same route.
|
|
68
|
+
def find(from_col, from_row, to_col, to_row) = @search.find(from_col, from_row, to_col, to_row)
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def shared(grid, width, height, solid)
|
|
73
|
+
return grid unless width || height || solid
|
|
74
|
+
|
|
75
|
+
raise ArgumentError, 'a NavGrid is built over a grid: or from width:, height: and solid:, not both'
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def private_grid(width, height, solid)
|
|
79
|
+
unless width && height && solid
|
|
80
|
+
raise ArgumentError, 'a NavGrid needs a grid:, or all of width:, height: and solid:'
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
Util::SolidGrid.build(width, height) { |col, row| solid.call(col, row) }
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|