rgame 0.1.0 → 0.2.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.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +94 -0
  3. data/README.md +130 -233
  4. data/docs/api/README.md +116 -69
  5. data/docs/api/assets.md +11 -12
  6. data/docs/api/components.md +58 -34
  7. data/docs/api/drawing.md +77 -9
  8. data/docs/api/game.md +34 -13
  9. data/docs/api/input.md +232 -51
  10. data/docs/api/scene_graph.md +242 -15
  11. data/docs/api/systems.md +20 -0
  12. data/docs/api/toolbox.md +19 -15
  13. data/docs/api/ui.md +98 -0
  14. data/docs/api/values.md +32 -0
  15. data/ext/README.md +6 -5
  16. data/ext/rgame_core/app/app.c +182 -8
  17. data/ext/rgame_core/audio/audio.c +74 -0
  18. data/ext/rgame_core/example.rb +17 -6
  19. data/ext/rgame_core/extconf.rb +52 -24
  20. data/ext/rgame_core/graphics/canvas.c +45 -4
  21. data/ext/rgame_core/graphics/canvas.h +65 -10
  22. data/ext/rgame_core/graphics/clip.c +22 -13
  23. data/ext/rgame_core/include/rgame/core.h +113 -3
  24. data/ext/rgame_core/input/gamepad.c +57 -3
  25. data/ext/rgame_core/ruby/core_ext.c +16 -0
  26. data/ext/rgame_core/ruby/renderer_ext.c +23 -0
  27. data/ext/rgame_util/color_ext.c +12 -3
  28. data/lib/rgame/core/app.rb +2 -0
  29. data/lib/rgame/core/input.rb +35 -41
  30. data/lib/rgame/core/recording.rb +3 -1
  31. data/lib/rgame/core/renderer.rb +76 -28
  32. data/lib/rgame/core/tile_map_renderer.rb +84 -55
  33. data/lib/rgame/engine/camera.rb +55 -10
  34. data/lib/rgame/engine/component.rb +11 -1
  35. data/lib/rgame/engine/components/animated_sprite.rb +9 -3
  36. data/lib/rgame/engine/components/camera_follow.rb +44 -0
  37. data/lib/rgame/engine/components/character_body.rb +25 -4
  38. data/lib/rgame/engine/components/sprite.rb +11 -1
  39. data/lib/rgame/engine/components/tile_world.rb +31 -18
  40. data/lib/rgame/engine/culling.rb +47 -0
  41. data/lib/rgame/engine/debug_overlay.rb +20 -9
  42. data/lib/rgame/engine/input/action_mapper.rb +101 -21
  43. data/lib/rgame/engine/input/actions.rb +69 -12
  44. data/lib/rgame/engine/input/input_map.rb +178 -0
  45. data/lib/rgame/engine/layout.rb +82 -0
  46. data/lib/rgame/engine/node2d.rb +205 -36
  47. data/lib/rgame/engine/player.rb +69 -0
  48. data/lib/rgame/engine/player_layer.rb +70 -0
  49. data/lib/rgame/engine/players.rb +212 -0
  50. data/lib/rgame/engine/scene/scene_stack.rb +25 -3
  51. data/lib/rgame/engine/spatial_hash.rb +17 -4
  52. data/lib/rgame/engine/tile_map_layer.rb +84 -0
  53. data/lib/rgame/engine/ui/menu.rb +115 -0
  54. data/lib/rgame/engine/ui/menu_item.rb +84 -0
  55. data/lib/rgame/engine/view.rb +76 -0
  56. data/lib/rgame/engine/viewports.rb +174 -0
  57. data/lib/rgame/engine/world_view.rb +70 -0
  58. data/lib/rgame/engine.rb +13 -1
  59. data/lib/rgame/game.rb +81 -11
  60. data/lib/rgame/util/controls.rb +117 -41
  61. data/lib/rgame/util/z.rb +133 -0
  62. data/lib/rgame/util.rb +1 -0
  63. data/lib/rgame/version.rb +1 -1
  64. metadata +26 -11
  65. data/lib/rgame/engine/camera_view.rb +0 -28
data/docs/api/game.md CHANGED
@@ -7,8 +7,8 @@ engine.
7
7
  $LOAD_PATH.unshift File.expand_path('../lib', __dir__)
8
8
  require 'rgame/game'
9
9
 
10
- class HelloScene < Engine::Node2D
11
- def on_draw(renderer) = renderer.text('Hello world!', 250, 200)
10
+ class HelloScene < RGame::Engine::Node2D
11
+ def on_draw(renderer, _view) = renderer.text('Hello world!', 250, 200)
12
12
  end
13
13
 
14
14
  RGame::Game.new(root: HelloScene.new, caption: 'Hello').start
@@ -20,19 +20,24 @@ sound device, the input mapper, the debug overlay — and drives the root node.
20
20
 
21
21
  ```ruby
22
22
  RGame::Game.new(root:, width: 640, height: 480, caption: 'RGame',
23
- media_root: 'media', action_map: {})
23
+ media_root: 'media', input_map: nil, device: Controls::KEYBOARD)
24
24
  ```
25
25
 
26
26
  | Reader | |
27
27
  |---|---|
28
28
  | `root` | the node tree |
29
29
  | `renderer` | what scenes draw through |
30
- | `action_mapper` | physical input named actions |
30
+ | `players` | who is playing: their devices, bindings and cameras |
31
31
  | `assets`, `audio`, `media_root`, `width`, `height`, `fps` | inherited from [App](app.md) |
32
32
 
33
33
  `start` brings the tree live — it hands the game to the root as its `context`,
34
- calls `enter_tree`, and runs the loop until the window closes. `Esc` quits and
35
- `F1` toggles the debug overlay.
34
+ calls `enter_tree`, and runs the loop until the window closes. `F1` toggles the
35
+ debug overlay and `F2` quits.
36
+
37
+ **Both development keys are function keys, and `Esc` is deliberately left
38
+ alone.** Escape is the button a player expects to back out of a menu, so it
39
+ belongs to the game rather than to the engine's debug shortcuts — binding it
40
+ here would take it away from every game built on this one.
36
41
 
37
42
  ## Why this class exists at all
38
43
 
@@ -57,19 +62,35 @@ sheet = node.root.context.assets.sheet('player.json')
57
62
 
58
63
  ## Input
59
64
 
60
- `action_map` names the actions a game has, in terms of the physical ids
61
- [Input](input.md) knows:
65
+ `input_map:` names the actions a game has, in terms of physical ids from
66
+ [`RGame::Util::Controls`](input.md):
62
67
 
63
68
  ```ruby
69
+ Controls = RGame::Util::Controls
70
+
64
71
  RGame::Game.new(
65
72
  root: Root.new,
66
- action_map: {
67
- move_x: { axis: %i[left right] }, # -1.0 .. 1.0
68
- fire: { button: %i[fire] } # held / pressed / released
69
- }
73
+ input_map: RGame::Engine::InputMap.new(
74
+ move_x: { axis: [Controls::KEY_LEFT, Controls::KEY_RIGHT], # -1.0 .. 1.0
75
+ stick: Controls::AXIS_LEFT_X },
76
+ fire: { buttons: [Controls::KEY_SPACE, Controls::PAD_A] } # held / pressed / released
77
+ )
70
78
  )
71
79
  ```
72
80
 
81
+ Pass nothing and you get [`InputMap.default`](input.md): eight-way `move_x` /
82
+ `move_y` on the arrows or the left stick, plus `fire`. Either way the map is
83
+ merged over the universal UI set, so `ui_confirm` and `ui_cancel` work without
84
+ being declared.
85
+
86
+ `device:` picks what drives player one — the keyboard by default, or
87
+ `Controls.gamepad(slot)` for a controller.
88
+
89
+ `players:` is how many seats the game has (default 1). Extra seats start empty
90
+ and fill when somebody picks up a controller and presses confirm; an empty seat
91
+ draws no viewport, so a two-seat game played by one person is an ordinary
92
+ full-screen game. See [Players, seats and joining](input.md#players-seats-and-joining).
93
+
73
94
  A scene reads the resulting snapshot in `on_control(actions)` — `actions.axis(:move_x)`,
74
95
  `actions.pressed?(:fire)` — and never sees a key.
75
96
 
@@ -92,7 +113,7 @@ behaviour around the tree rather than replacing the shell.
92
113
  ```ruby
93
114
  class MyGame < RGame::Game
94
115
  def button_down(id)
95
- super # keeps Esc and F1 working
116
+ super # keeps F1 and F2 working
96
117
  @paused = !@paused if id == RGame::Util::Controls::KEY_SPACE
97
118
  end
98
119
  end
data/docs/api/input.md CHANGED
@@ -1,85 +1,240 @@
1
1
  # Input
2
2
 
3
- Three pieces work together:
3
+ Four pieces, in two layers:
4
4
 
5
5
  - **`RGame::Util::Controls`** — the vocabulary: which number means "the left
6
6
  arrow key", "the A button", "player 2's controller". Plain values, usable
7
7
  without loading any graphics library.
8
- - **`RGame::Core::Input`** — translates your game's *actions* (`:fire`,
9
- `:confirm`) into those ids and asks the app whether they are held.
10
- - **`RGame::Core::Gamepad`** — which controllers are plugged in, for menus.
8
+ - **`RGame::Core::Input`** — the raw query: is *this id* active on *this
9
+ device*.
10
+ - **`RGame::Engine::InputMap`** — what those ids *mean*: one table per player,
11
+ mapping a game's actions onto physical ids.
12
+ - **`RGame::Engine::ActionMapper`** — polls one player's device through their
13
+ map once per tick and produces an `Actions` snapshot.
14
+
15
+ Plus **`RGame::Core::Gamepad`**, a readout of which controllers are plugged in,
16
+ for menus.
11
17
 
12
18
  There is no mouse support, by design.
13
19
 
14
- ## `RGame::Core::Input`
20
+ ## Which layer do I want?
15
21
 
16
- ```ruby
17
- input = RGame::Core::Input.new(app)
22
+ Almost always the engine layer. A game declares its actions, reads
23
+ `actions.held?(:fire)`, and never names a scancode outside its input map.
24
+ `RGame::Core::Input` is what the mapper polls; you reach for it directly only
25
+ when writing against `RGame::Core` alone, with no scene graph.
18
26
 
19
- input.down?(:fire) # keyboard — the single-player default
20
- input.down?(:fire, device: pad) # a specific controller
21
- input.axis(:move_x, device: pad) # => Float
27
+ ```ruby
28
+ RGame::Game.new(
29
+ root: MyRoot.new,
30
+ input_map: RGame::Engine::InputMap.new(
31
+ fire: { buttons: [Controls::KEY_SPACE, Controls::PAD_A] }
32
+ )
33
+ )
22
34
  ```
23
35
 
24
- `down?` and `axis` read a snapshot the engine takes **once per frame**, when it
25
- pumps events. That is what makes them safe to call from `update`: a frame can
26
- run several simulation ticks, and every tick sees the same answer. Reading
27
- hardware directly would make a held key behave differently depending on how
28
- slow the previous frame was.
36
+ ## `RGame::Engine::InputMap`
37
+
38
+ One entry per action, naming physical ids directly. **This is the single table a
39
+ rebinding screen edits.**
40
+
41
+ ```ruby
42
+ Controls = RGame::Util::Controls
29
43
 
30
- ### Actions, not keys
44
+ map = RGame::Engine::InputMap.new(
45
+ turn: { axis: [Controls::KEY_LEFT, Controls::KEY_RIGHT], stick: Controls::AXIS_LEFT_X },
46
+ thrust: { axis: [Controls::KEY_DOWN, Controls::KEY_UP], stick: Controls::AXIS_TRIGGER_RIGHT },
47
+ fire: { buttons: [Controls::KEY_SPACE, Controls::PAD_A] }
48
+ )
49
+ ```
31
50
 
32
- You ask for `:fire`, not for the space bar. The mapping lives in a binding
33
- table, so the same game code works for a keyboard player and a controller
34
- player:
51
+ Three kinds of source, and one action may combine them:
35
52
 
36
- | Action | Keyboard | Gamepad |
53
+ | Key | Read with | Meaning |
37
54
  |---|---|---|
38
- | `:left` `:right` `:up` `:down` | arrow keys | dpad |
39
- | `:confirm` | Return | A |
40
- | `:fire` | Space | A |
55
+ | `buttons:` | `held?` / `pressed?` / `released?` | down if **any** listed id is down |
56
+ | `axis:` | `axis` | `[negative_id, positive_id]` — a digital axis from two buttons |
57
+ | `stick:` | `axis` | an analog axis id, for a real stick or a trigger |
41
58
 
42
- Asking for an action nothing is bound to raises `KeyError`.
59
+ When an action binds both `axis:` and `stick:`, **the larger deflection wins**.
60
+ That needs no per-device branching: a keyboard reads `0.0` for every axis and a
61
+ stick reads `false` for every key, so whichever device a player is on, the other
62
+ source contributes nothing.
43
63
 
44
- ### Devices
64
+ ### One table serves every device
45
65
 
46
- Device 0 is the keyboard, and it is the default so single-player code never
47
- mentions devices at all. Controllers follow, one per player slot:
66
+ Listing a key and a pad button in the same entry is safe, because **a device
67
+ only answers for its own kind of input** asking a gamepad about a keyboard
68
+ scancode is `false`, never the keyboard's answer. So `fire` can be "Space or A",
69
+ and each player's device picks out the half that applies to it.
70
+
71
+ ### A stick's sign is the device's
72
+
73
+ `AXIS_LEFT_Y` is positive **downwards**, like screen coordinates. An action that
74
+ wants the opposite ("thrust", "climb") negates at the call site or binds a
75
+ trigger instead — the map stays declarative rather than growing an inversion
76
+ flag every reader would have to check for.
77
+
78
+ ### The universal UI set
79
+
80
+ Every map is merged over a universal set, so these exist whether or not a game
81
+ declares them:
82
+
83
+ `ui_up`, `ui_down`, `ui_left`, `ui_right`, `ui_confirm`, `ui_cancel`
84
+
85
+ Keyboard navigation and menus rely on them being there for **every** player. They
86
+ are prefixed so a game is free to use `:up` for something of its own, and a game
87
+ that wants different bindings just declares one:
48
88
 
49
89
  ```ruby
50
- Controls = RGame::Util::Controls
90
+ InputMap.new(ui_confirm: { buttons: [Controls::PAD_X] })
91
+ ```
51
92
 
52
- Controls::KEYBOARD # => 0
53
- Controls.gamepad(0) # the first controller
54
- Controls.gamepad(1) # the second
55
- Controls::MAX_GAMEPADS # how many slots exist
93
+ `ui_cancel` is Escape — which is why `RGame::Game`'s quit key is `F2`. The button
94
+ a player expects to back out of a menu belongs to the menu.
95
+
96
+ ### Defaults and rebinding
97
+
98
+ `InputMap.default` is the UI set plus eight-way movement (`move_x`, `move_y`) on
99
+ the arrows or the left stick, and `fire`. A game wanting exactly that passes no
100
+ `input_map:` at all.
101
+
102
+ `#merge` returns a copy with some actions replaced, which is how a config screen
103
+ rebinds one without restating the rest:
104
+
105
+ ```ruby
106
+ map = RGame::Engine::InputMap.default.merge(fire: { buttons: [Controls::KEY_RETURN] })
56
107
  ```
57
108
 
58
- A device only answers for its own kind of input. Asking a gamepad about a
59
- keyboard key is `false`, never the keyboard's answer otherwise player two's
60
- pad would echo player one. The keyboard has no axes, so `axis` on it is `0.0`.
109
+ A malformed entry raises at construction an unknown source key, an entry with
110
+ no source, an empty button list, an axis that is not a pair. That is deliberate:
111
+ the alternative is an action that reads as "never pressed" for the rest of the
112
+ program, discovered as a frame nobody can move in.
61
113
 
62
- ### Rebinding
114
+ ## `RGame::Engine::ActionMapper`
63
115
 
64
- A binding table is just a Hash of action to id, and the ids are ordinary values
65
- from `Util`, so a game can build its own and hand it over:
116
+ One per player. It polls that player's device through their map and returns the
117
+ `Actions` snapshot game logic reads.
66
118
 
67
119
  ```ruby
68
- controls = RGame::Util::Controls
69
- bindings = controls::DEFAULT_KEYBOARD.merge(fire: controls::KEY_RETURN)
120
+ mapper = RGame::Engine::ActionMapper.new(map, device: Controls.gamepad(0))
121
+ actions = mapper.poll(input)
70
122
 
71
- input = RGame::Core::Input.new(app, bindings: bindings)
123
+ actions.held?(:fire) # is it down now
124
+ actions.pressed?(:fire) # did it go down this tick
125
+ actions.released?(:fire) # did it come up this tick
126
+ actions.axis(:turn) # -1.0..1.0
72
127
  ```
73
128
 
74
- `Input.new` accepts three optional tables:
129
+ **The device is what makes two players work.** Every query carries it, so two
130
+ mappers over the *same* map read two different controllers, and each keeps its
131
+ own previous-frame state so their edge queries are independent. Reassign
132
+ `mapper.device` to follow a hot-plug.
133
+
134
+ `dead_zone:` (default `0.15`) ignores a resting stick, which genuinely reports
135
+ small non-zero values. It **rescales** rather than merely cutting off, so a stick
136
+ leaving the dead zone ramps from zero instead of jumping to `0.15`.
137
+
138
+ `RGame::Game` builds one of these for you and polls it once per tick; a game
139
+ normally sees only the `Actions` handed to `control`.
140
+
141
+ ## Players, seats and joining
142
+
143
+ `RGame::Engine::Players` is a root-scoped system holding who is playing. Each
144
+ `RGame::Engine::Player` owns a device, an `InputMap`, a camera and a UI root —
145
+ the action *names* are the game's and shared, the buttons behind them are not.
146
+
147
+ ```ruby
148
+ RGame::Game.new(root: MyRoot.new, players: 2)
149
+ ```
75
150
 
76
- | Keyword | Default | Used for |
151
+ `players:` is how many **seats** the game has, and therefore the most people who
152
+ can play it. Player 0 starts on the keyboard; the rest start empty. An empty
153
+ seat draws no viewport, so a two-seat game with nobody in the second one is an
154
+ ordinary full-screen single-player game.
155
+
156
+ ### A device is seated when someone uses it
157
+
158
+ Not when it is plugged in. Plugging a controller in says something about
159
+ hardware; seating a player creates a camera, a viewport and a screen split, and
160
+ that follows a statement of intent — a **`ui_confirm` press** on the device.
161
+
162
+ One action rather than "any input", because a stick resting slightly off centre
163
+ must never seat a player. An edge rather than held, so one press does one thing.
164
+ It is read through the map of whoever would receive the device, so rebinding
165
+ `ui_confirm` rebinds "press to join" with it.
166
+
167
+ ```ruby
168
+ players = node.system(RGame::Engine::Players)
169
+
170
+ players.on_unassigned_input = :join # :join | :takeover | :ignore
171
+ players.accepting_joins = false # temporarily refuse
172
+ players.on_joined { |player| spawn(player) }
173
+ ```
174
+
175
+ | Policy | A press on a device nobody holds | Default when |
77
176
  |---|---|---|
78
- | `bindings:` | `Controls::DEFAULT_KEYBOARD` | keyboard buttons |
79
- | `pad_bindings:` | `Controls::DEFAULT_PAD` | controller buttons |
80
- | `axis_bindings:` | `Controls::DEFAULT_AXES` | analog axes |
177
+ | `:join` | fills the next free seat | there is more than one seat |
178
+ | `:takeover` | becomes the **primary** player's device | there is one seat |
179
+ | `:ignore` | nothing; the game calls `players.seat(device)` itself | |
180
+
181
+ `:takeover` is single-player's answer: one person already playing who picks up a
182
+ controller is not a second person arriving. Their keyboard becomes unassigned,
183
+ so using it again takes them back — last device used wins. And if their
184
+ controller is unplugged they fall back to the keyboard rather than the game
185
+ going dead in their hands.
186
+
187
+ `accepting_joins = false` refuses both, which is what a cutscene or a mid-round
188
+ lockout wants.
189
+
190
+ `on_joined` fires with the player who got the device, which is how a scene
191
+ spawns their character without polling for one. See `examples/15_tiled_world`.
192
+
193
+ ## `RGame::Core::Input`
194
+
195
+ The raw query, and deliberately nothing more.
196
+
197
+ ```ruby
198
+ input = RGame::Core::Input.new(app)
199
+
200
+ input.down?(Controls::KEY_SPACE) # keyboard
201
+ input.down?(Controls::PAD_A, device: Controls.gamepad(0)) # player 1's pad
202
+ input.axis(Controls::AXIS_LEFT_X, device: Controls.gamepad(0))
203
+ ```
81
204
 
82
- The defaults are frozen, so `merge` a copy rather than mutating them.
205
+ `down?` and `axis` read a snapshot the engine takes **once per frame**, when it
206
+ pumps events. That is what makes them safe to call from `update`: a frame can run
207
+ several simulation ticks, and every tick sees the same answer. Reading hardware
208
+ directly would make a held key behave differently depending on how slow the
209
+ previous frame was.
210
+
211
+ Ids are numbers, and they cross into C, so passing anything else raises
212
+ `TypeError`. No dead zone is applied here — this is the hardware's answer.
213
+
214
+ ### It used to hold the binding tables
215
+
216
+ It took `down?(:fire)` and resolved `:fire` through one of three tables passed to
217
+ its constructor. Those tables are gone. Binding moved up to `InputMap` for two
218
+ reasons: a rebinding screen has to be able to edit the table, and the engine
219
+ layer may not name `RGame::Core` at all; and with a player per device, the table
220
+ is a per-player value rather than a property of the one object that talks to the
221
+ hardware.
222
+
223
+ ### Devices
224
+
225
+ Device 0 is the keyboard, and it is the default — so single-player code never
226
+ mentions devices at all. Controllers follow, one per player slot:
227
+
228
+ ```ruby
229
+ Controls::KEYBOARD # => 0
230
+ Controls.gamepad(0) # the first controller
231
+ Controls.gamepad(1) # the second
232
+ Controls::MAX_GAMEPADS # how many slots exist
233
+ ```
234
+
235
+ A device only answers for its own kind of input. Asking a gamepad about a
236
+ keyboard key is `false`, never the keyboard's answer — otherwise player two's pad
237
+ would echo player one. The keyboard has no axes, so `axis` on it is `0.0`.
83
238
 
84
239
  ## `RGame::Util::Controls`
85
240
 
@@ -87,14 +242,39 @@ The id vocabulary. Available from `require 'rgame'` **and** from
87
242
  `require 'rgame/core'`, because these are plain integers with nothing behind
88
243
  them — a game's configuration screen can name a key without pulling in a window.
89
244
 
90
- **Keys** — `KEY_LEFT`, `KEY_RIGHT`, `KEY_UP`, `KEY_DOWN`, `KEY_RETURN`,
91
- `KEY_SPACE`, `KEY_ESCAPE`, `KEY_F1`.
245
+ **Keys** — what a Western keyboard can be relied on to have, 81 of them:
246
+
247
+ | | |
248
+ |---|---|
249
+ | Letters | `KEY_A` … `KEY_Z` |
250
+ | Digits | `KEY_1` … `KEY_9`, `KEY_0` |
251
+ | Whitespace and editing | `KEY_RETURN`, `KEY_ESCAPE`, `KEY_BACKSPACE`, `KEY_TAB`, `KEY_SPACE` |
252
+ | Punctuation | `KEY_MINUS`, `KEY_EQUALS`, `KEY_LEFTBRACKET`, `KEY_RIGHTBRACKET`, `KEY_BACKSLASH`, `KEY_SEMICOLON`, `KEY_APOSTROPHE`, `KEY_GRAVE`, `KEY_COMMA`, `KEY_PERIOD`, `KEY_SLASH` |
253
+ | Function row | `KEY_CAPSLOCK`, `KEY_F1` … `KEY_F12` |
254
+ | Navigation | `KEY_INSERT`, `KEY_HOME`, `KEY_PAGEUP`, `KEY_DELETE`, `KEY_END`, `KEY_PAGEDOWN` |
255
+ | Arrows | `KEY_LEFT`, `KEY_RIGHT`, `KEY_UP`, `KEY_DOWN` |
256
+ | Modifiers | `KEY_LCTRL`, `KEY_LSHIFT`, `KEY_LALT`, `KEY_RCTRL`, `KEY_RSHIFT`, `KEY_RALT` |
257
+
258
+ **A scancode is a position, not a letter.** `KEY_A` is the key marked A on a
259
+ QWERTY board and Q on AZERTY — which is what you want for `WASD` movement, and
260
+ what a rebinding screen has to explain to the player. The engine only ever
261
+ compares numbers.
262
+
263
+ **Deliberately absent**: the numpad (most laptops have none), the GUI key
264
+ (Windows on a PC, Command on a Mac), the print-screen cluster, and anything
265
+ whose position depends on the layout. Adding one is a `#define` in
266
+ `ext/rgame_core/include/rgame/core.h`, a `_Static_assert` against the SDL
267
+ scancode, and a constant here — and the spec below checks all three agree.
92
268
 
93
269
  **Gamepad buttons** — `PAD_A`, `PAD_B`, `PAD_X`, `PAD_Y`, `PAD_BACK`,
94
270
  `PAD_GUIDE`, `PAD_START`, `PAD_LEFT_STICK`, `PAD_RIGHT_STICK`,
95
271
  `PAD_LEFT_SHOULDER`, `PAD_RIGHT_SHOULDER`, `PAD_DPAD_UP`, `PAD_DPAD_DOWN`,
96
272
  `PAD_DPAD_LEFT`, `PAD_DPAD_RIGHT`.
97
273
 
274
+ Plus the ones only some hardware has, which read as never pressed on a pad
275
+ without them: `PAD_MISC1` (share/capture/microphone), `PAD_PADDLE1` …
276
+ `PAD_PADDLE4` (Xbox Elite), `PAD_TOUCHPAD` (PS4/PS5).
277
+
98
278
  **Axes** — `AXIS_LEFT_X`, `AXIS_LEFT_Y`, `AXIS_RIGHT_X`, `AXIS_RIGHT_Y`,
99
279
  `AXIS_TRIGGER_LEFT`, `AXIS_TRIGGER_RIGHT`. Sticks read −1.0 to 1.0 with **y
100
280
  positive downwards**; triggers read 0.0 to 1.0. No dead zone is applied — where
@@ -104,7 +284,8 @@ non-zero values.
104
284
  **Devices** — `KEYBOARD`, `GAMEPAD_FIRST`, `MAX_GAMEPADS`, and
105
285
  `Controls.gamepad(slot)`.
106
286
 
107
- **Default tables**`DEFAULT_KEYBOARD`, `DEFAULT_PAD`, `DEFAULT_AXES`.
287
+ This module is the **vocabulary only**. It carries no binding tables — what an id
288
+ *means* is `RGame::Engine::InputMap`, one per player.
108
289
 
109
290
  Buttons and keys share one numbering, partitioned into ranges, so a single
110
291
  "is it held" query serves every device. You never need the numbers themselves —
@@ -170,7 +351,7 @@ class MyGame < RGame::Core::App
170
351
  end
171
352
 
172
353
  def update(_dt)
173
- @moving_left = @input.down?(:left, device: @device)
354
+ @moving_left = @input.down?(RGame::Util::Controls::KEY_LEFT, device: @device)
174
355
  end
175
356
  end
176
357
  ```