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
@@ -0,0 +1,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RGame
4
+ module Engine
5
+ # How the screen is divided, as a root-scoped system.
6
+ #
7
+ # node.system(Viewports).views # one View per active player
8
+ # node.system(Viewports).screen # the whole window, no camera
9
+ # node.system(Viewports).solo!(cam) # collapse to one view — cutscene
10
+ #
11
+ # It holds the mutable half of the question — which mode is current, who is
12
+ # playing, how big the window is — while Layout holds the arithmetic. That
13
+ # split is deliberate: the rect maths is pure and gets specced with no tree
14
+ # and no window, and only the state that genuinely changes lives in a
15
+ # component.
16
+ #
17
+ # It is a Component on the root, so any node reaches it by walking the tree
18
+ # rather than having it threaded through a constructor — the same shape
19
+ # CollisionWorld and Players use.
20
+ #
21
+ # ## Mode changes are deferred
22
+ #
23
+ # `solo!` and `split!` record a request; it is applied in `update`, which
24
+ # runs in the root's component phase. That matters because this is reachable
25
+ # from anywhere, including from a `draw` — and a `draw` now runs once per
26
+ # view, so a mode change made there would fire several times and tear the
27
+ # frame it was made in. Deferring is the same shape `queue_free` uses, and it
28
+ # means a change takes effect on the next tick.
29
+ class Viewports < Component
30
+ # `views` is the list drawn through this frame — one per active player
31
+ # while split, exactly one while solo. Reused, like the Views in it.
32
+ attr_reader :screen, :width, :height, :views
33
+
34
+ def initialize(players, width: 0, height: 0)
35
+ super()
36
+ @players = players
37
+ @width = width
38
+ @height = height
39
+ @solo_camera = nil
40
+ @pending = nil
41
+ # One View per possible player for the world, one more each for their own
42
+ # screen space, and one for the whole window. Built once and mutated in
43
+ # place. See View: these are reused, never rebuilt.
44
+ @pool = []
45
+ @screen_pool = []
46
+ @views = []
47
+ @screen = View.new
48
+ refresh
49
+ end
50
+
51
+ # The window changed size. Rects are recomputed from it on the next
52
+ # refresh, and every camera reclamps against its new rect — which is why
53
+ # a camera does not carry one.
54
+ def resize(width, height)
55
+ @width = width
56
+ @height = height
57
+ refresh
58
+ end
59
+
60
+ def solo? = !@solo_camera.nil?
61
+
62
+ # The screen-space region belonging to `player`: the same rectangle their
63
+ # world view is drawn into, with **no camera**, so its contents are laid
64
+ # out against their own corner rather than the window's. Their HUD and
65
+ # their menus live here.
66
+ #
67
+ # `nil` when they have no region to draw into, which is two cases and one
68
+ # answer. An **empty seat** has no viewport at all. And while the split is
69
+ # **collapsed**, nobody has a half of the screen to own: a cutscene is
70
+ # everyone looking at one thing, so per-player UI has no place to be, and
71
+ # a game wanting something on screen through it draws in the global
72
+ # overlay band instead.
73
+ #
74
+ # Returning nil rather than an empty rectangle is deliberate: one check at
75
+ # the one caller that needs it beats every caller relying on a zero-sized
76
+ # clip happening to draw nothing.
77
+ def screen_for(player)
78
+ return nil if player.nil? || @solo_camera
79
+
80
+ world = @views.find { |view| view.player.equal?(player) }
81
+ return nil if world.nil?
82
+
83
+ pooled_screen(@players.list.index(player))
84
+ .set(world.x, world.y, world.width, world.height, player: player)
85
+ end
86
+
87
+ # Collapse to a single screen-wide view through `camera`.
88
+ #
89
+ # The camera is **required**: promoting one player's would silently give
90
+ # everyone else their view, and deciding what is on screen is what a
91
+ # cutscene is for. A game points an ordinary Camera wherever it likes —
92
+ # with a CameraFollow on a cutscene actor, or its own component framing
93
+ # every player at once — and hands it here.
94
+ def solo!(camera)
95
+ raise ArgumentError, 'solo! needs a camera to look through' if camera.nil?
96
+
97
+ @pending = camera
98
+ self
99
+ end
100
+
101
+ # Back to one view per player.
102
+ def split! = @pending = :split
103
+
104
+ # Applies a pending mode change, then rebuilds the rects. Runs in the
105
+ # root's component phase, so a change requested during a tick lands on the
106
+ # next one.
107
+ def update(_dt)
108
+ apply_pending
109
+ refresh
110
+ end
111
+
112
+ # Recompute every rect from the current mode and window, and reclamp each
113
+ # camera against the rect it is about to be drawn into.
114
+ #
115
+ # Allocation-free once the pool has grown: Layout yields its rects rather
116
+ # than building them, and the Views are mutated in place.
117
+ def refresh
118
+ @screen.set(0, 0, @width, @height)
119
+ @solo_camera ? refresh_solo : refresh_split
120
+ @views.each { |view| view.camera&.resolve(view.width, view.height) }
121
+ self
122
+ end
123
+
124
+ private
125
+
126
+ def apply_pending
127
+ return if @pending.nil?
128
+
129
+ @solo_camera = @pending == :split ? nil : @pending
130
+ @pending = nil
131
+ end
132
+
133
+ def refresh_solo
134
+ @views.clear
135
+ @views << pooled(0).set(0, 0, @width, @height, camera: @solo_camera)
136
+ end
137
+
138
+ def refresh_split
139
+ @views.clear
140
+ count = @players.active_count
141
+ return if count.zero?
142
+
143
+ index = 0
144
+ Layout.each_rect(count, @width, @height) do |i, x, y, w, h|
145
+ player = active_at(i)
146
+ @views << pooled(index).set(x, y, w, h, camera: player.camera, player: player)
147
+ index += 1
148
+ end
149
+ end
150
+
151
+ # `each_active` is a filter, so it cannot be indexed without building an
152
+ # Array. Walking it costs nothing and this runs once per frame.
153
+ def active_at(index)
154
+ found = nil
155
+ i = 0
156
+ @players.each do |player|
157
+ next unless player.active?
158
+
159
+ found = player if i == index
160
+ i += 1
161
+ end
162
+ found
163
+ end
164
+
165
+ def pooled(index)
166
+ @pool[index] ||= View.new
167
+ end
168
+
169
+ def pooled_screen(index)
170
+ @screen_pool[index] ||= View.new
171
+ end
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RGame
4
+ module Engine
5
+ # The node that marks where world space begins.
6
+ #
7
+ # view = scene.add_node(WorldView.new)
8
+ # view.add_node(player) # draws at its own world coordinates
9
+ #
10
+ # Its children live in **world** coordinates and are drawn once per active
11
+ # viewport: for each one, clip to that viewport's rectangle and translate by
12
+ # its camera, then run the same subtree again. Everything outside a
13
+ # WorldView is screen space and draws once.
14
+ #
15
+ # That is the whole of split-screen, and it is why the transform and clip
16
+ # stacks in Core are proper push/pop stacks — a clip always narrows, so a
17
+ # child can never draw outside the region its parent allowed.
18
+ #
19
+ # ## The subtree does not know how many times it is drawn
20
+ #
21
+ # Children draw at their own absolute origin and never see a camera. This
22
+ # node supplies the view, so the same world serves one player or four with
23
+ # nothing below it changing — which is exactly what a camera owned by a node
24
+ # *inside* the world could not do, and why cameras belong to players.
25
+ #
26
+ # ## Only `draw` multiplies, and deliberately
27
+ #
28
+ # `control` and `update` still run **once** for this subtree, however many
29
+ # viewports draw it. That is what keeps simulation cost independent of player
30
+ # count, and it is also a safety property: a world node's `on_update` is
31
+ # where mutation belongs, so per-view updating would move an actor at twice
32
+ # the speed with two players — correctly in single player, silently wrong the
33
+ # moment somebody joins.
34
+ #
35
+ # It does make the standing "draw renders state" rule load-bearing rather
36
+ # than stylistic: a `draw` with a side effect now happens once per player,
37
+ # and an allocation in one costs that many times.
38
+ #
39
+ # A node that genuinely needs per-view work at draw time can read
40
+ # `system(Viewports).views` itself. Anything needing per-view state *over
41
+ # time* — a screen shake, a hit flash — is per-*player* rather than per-view,
42
+ # and belongs on that player's camera or their own subtree, which tick once.
43
+ class WorldView < Node2D
44
+ # World content, which is the default anyway — stated because this is the
45
+ # node that marks where world space begins, and a reader looking for
46
+ # "which band is the world in" should find the answer here.
47
+ def initialize(**)
48
+ super(band: :world, **)
49
+ end
50
+
51
+ # Drawn once per viewport, so this overrides the whole of `draw` rather
52
+ # than just `draw_children`: the node's own visuals belong inside the
53
+ # viewport too, not once outside all of them.
54
+ #
55
+ # `view` is the screen-space view this node was reached with, and is
56
+ # deliberately ignored — a WorldView asks the system which viewports exist
57
+ # rather than being told, so a game can place one anywhere in the tree.
58
+ def draw(renderer, _view = nil)
59
+ viewports = system(Viewports)
60
+ viewports.views.each do |world_view|
61
+ renderer.clipped(world_view.x, world_view.y, world_view.width, world_view.height) do
62
+ renderer.translated(world_view.offset_x, world_view.offset_y) do
63
+ super(renderer, world_view)
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
data/lib/rgame/engine.rb CHANGED
@@ -19,6 +19,7 @@
19
19
  # three-layer rule allows and this file therefore has to declare.
20
20
  require_relative 'util'
21
21
 
22
+ require_relative 'engine/culling'
22
23
  require_relative 'engine/matrix'
23
24
  require_relative 'engine/signal'
24
25
  require_relative 'engine/node2d'
@@ -26,7 +27,12 @@ require_relative 'engine/component'
26
27
  require_relative 'engine/animation_set'
27
28
  require_relative 'engine/animator'
28
29
  require_relative 'engine/camera'
29
- require_relative 'engine/camera_view'
30
+ require_relative 'engine/layout'
31
+ require_relative 'engine/view'
32
+ require_relative 'engine/world_view'
33
+ require_relative 'engine/player_layer'
34
+ require_relative 'engine/ui/menu_item'
35
+ require_relative 'engine/ui/menu'
30
36
  require_relative 'engine/path'
31
37
  require_relative 'engine/timer'
32
38
  require_relative 'engine/tileset'
@@ -46,8 +52,12 @@ require_relative 'engine/debug_overlay'
46
52
  require_relative 'engine/body'
47
53
  require_relative 'engine/actor'
48
54
  require_relative 'engine/input/actions'
55
+ require_relative 'engine/input/input_map'
49
56
  require_relative 'engine/input/action_mapper'
50
57
  require_relative 'engine/input/player_controller'
58
+ require_relative 'engine/player'
59
+ require_relative 'engine/players'
60
+ require_relative 'engine/viewports'
51
61
  require_relative 'engine/scene/scene_stack'
52
62
  require_relative 'engine/components/velocity'
53
63
  require_relative 'engine/components/pool'
@@ -66,3 +76,5 @@ require_relative 'engine/components/character_body'
66
76
  require_relative 'engine/components/player_controller'
67
77
  require_relative 'engine/components/wander_controller'
68
78
  require_relative 'engine/components/animated_sprite'
79
+ require_relative 'engine/components/camera_follow'
80
+ require_relative 'engine/tile_map_layer'
data/lib/rgame/game.rb CHANGED
@@ -43,22 +43,61 @@ module RGame
43
43
  WIDTH = 640
44
44
  HEIGHT = 480
45
45
 
46
- attr_reader :root, :renderer, :action_mapper
46
+ attr_reader :root, :renderer
47
47
 
48
+ # `input_map:` is what physical inputs mean — one entry per action, naming
49
+ # ids from RGame::Util::Controls. It is merged over the universal UI set, so
50
+ # `ui_confirm` and friends work whether or not a game declares them, and it
51
+ # defaults to RGame::Engine::InputMap::DEFAULT_ACTIONS, so a game wanting
52
+ # eight-way movement and a fire button declares nothing.
53
+ #
54
+ # `device:` is which device drives it — the keyboard, or
55
+ # `Controls.gamepad(slot)` for a controller.
56
+ #
57
+ # `input:` overrides the input backend. It exists so a harness can drive a
58
+ # game from a script instead of from hardware — see tools/drive_example.rb,
59
+ # and CLAUDE.md's "The examples are the acceptance test for wiring", which
60
+ # is why driving one has to be possible at all. A game passes nothing and
61
+ # gets the real thing.
62
+ # `players:` is how many seats the game has, and therefore the most people
63
+ # who can play it. Player 0 starts on `device:`; the rest start empty and
64
+ # are filled when someone uses a controller — see RGame::Engine::Players for
65
+ # why that is a press rather than a plug.
48
66
  def initialize(root:, width: WIDTH, height: HEIGHT, caption: 'RGame',
49
- media_root: 'media', action_map: {})
67
+ media_root: 'media', input_map: nil, device: Controls::KEYBOARD,
68
+ players: 1, input: nil)
50
69
  super(width: width, height: height, caption: caption, media_root: media_root)
51
70
 
52
71
  @root = root
53
72
  @renderer = RGame::Core::Renderer.new(self)
54
- @input = RGame::Core::Input.new(self)
55
- @action_mapper = RGame::Engine::ActionMapper.new(action_map)
56
- @overlay = RGame::Engine::DebugOverlay.new # always wired up; F1 reveals it
73
+ @input = input || RGame::Core::Input.new(self)
74
+ @players = RGame::Engine::Players.new(
75
+ Array.new(players) do |id|
76
+ RGame::Engine::Player.new(id: id, device: id.zero? ? device : nil,
77
+ input_map: input_map)
78
+ end
79
+ )
80
+ @viewports = RGame::Engine::Viewports.new(@players, width: width, height: height)
81
+ @debug = RGame::Engine::DebugOverlay.new # always wired up; F1 reveals it
57
82
  @dirty = true # draw the first frame
58
83
 
59
84
  install_asset_loaders
60
85
  end
61
86
 
87
+ # The player registry, also reachable from any node as
88
+ # `node.system(RGame::Engine::Players)` — which is how a scene gets at a
89
+ # camera to follow, without anything being threaded into its constructor.
90
+ #
91
+ # One player exists from the start, so a single-player game never mentions
92
+ # players at all: it is `players.primary` that an unowned node reads from,
93
+ # and `players.primary.camera` that a scene points at its hero.
94
+ attr_reader :players
95
+
96
+ # How the screen is divided. Reachable as `node.system(RGame::Engine::Viewports)`,
97
+ # which is how a cutscene deep in a scene collapses the split without
98
+ # anything being handed to it.
99
+ attr_reader :viewports
100
+
62
101
  # Brings the tree live and runs until the window closes.
63
102
  #
64
103
  # The root gets this object as its `context`, which is how a node deep in
@@ -66,6 +105,10 @@ module RGame
66
105
  # anything being threaded through its constructor.
67
106
  def start
68
107
  @root.context = self
108
+ # Root-scoped systems, mounted before the tree comes alive so that an
109
+ # on_add anywhere in it can already resolve node.system(...) for either.
110
+ @root.add_component(@players)
111
+ @root.add_component(@viewports)
69
112
  @root.enter_tree # components attach, then on_add
70
113
  run
71
114
  end
@@ -86,7 +129,10 @@ module RGame
86
129
  # read identical state, and the edge lands on the first of them — one press,
87
130
  # one `pressed?`, which is what a caller means.
88
131
  def update(dt)
89
- @root.control(@action_mapper.poll(@input))
132
+ @players.poll(@input)
133
+ # The registry, not one player's snapshot: each node resolves the actions
134
+ # of whoever owns it, and a node that claims nobody gets the primary.
135
+ @root.control(@players)
90
136
  @root.update(dt)
91
137
  @root.sweep_freed # flush queue_free'd nodes outside the update traversal
92
138
  @dirty = true
@@ -94,17 +140,41 @@ module RGame
94
140
 
95
141
  # Only the simulation advancing makes the frame stale. While the overlay is
96
142
  # up, redraw anyway, so its numbers stay live even when nothing is moving.
97
- def needs_redraw? = @dirty || @overlay.visible?
143
+ def needs_redraw? = @dirty || @debug.visible?
98
144
 
145
+ # The tree is drawn once, with the whole window as its view. Screen-space
146
+ # content — a HUD, a menu, a title card — lands there and is drawn exactly
147
+ # once, as it always was.
148
+ #
149
+ # **World content multiplies inside the tree, not here.** An
150
+ # RGame::Engine::WorldView draws its subtree once per viewport, clipping and
151
+ # translating for each, so where the world begins is the game's choice
152
+ # rather than a shape the platform imposes. That is also what keeps
153
+ # `node.root` meaning the game's own root: nothing is inserted above it.
99
154
  def draw
100
- @root.draw(@renderer)
101
- @overlay.draw(@renderer, width, height, fps) # last, so it layers on top
155
+ @viewports.refresh # rects from the layout, then reclamp every camera
156
+ @root.draw(@renderer, @viewports.screen)
157
+ @debug.draw(@renderer, @viewports.screen, fps) # last, so it layers on top
102
158
  @dirty = false
103
159
  end
104
160
 
161
+ # The window changed size, so every rect and every camera clamp does too.
162
+ def resize(width, height) = @viewports.resize(width, height)
163
+
164
+ # Hot-plug is bookkeeping, not seating: a controller arriving becomes a
165
+ # device the registry watches, and it is someone *using* it that gives it to
166
+ # a player. Leaving takes it back off whoever had it.
167
+ def gamepad_connected(slot) = @players.device_connected(slot)
168
+ def gamepad_disconnected(slot) = @players.device_disconnected(slot)
169
+
170
+ # The two development keys, both function keys on purpose: **Escape is
171
+ # deliberately not bound here**, because it is the natural `cancel`/`back`
172
+ # button for a game's own menus, and a debug shortcut has no business taking
173
+ # the one key every player expects to close a dialog. F1 shows the overlay,
174
+ # F2 quits.
105
175
  def button_down(id)
106
- close if id == Controls::KEY_ESCAPE
107
- @overlay.toggle if id == Controls::KEY_F1
176
+ close if id == Controls::KEY_F2
177
+ @debug.toggle if id == Controls::KEY_F1
108
178
  end
109
179
 
110
180
  private
@@ -10,8 +10,14 @@ module RGame
10
10
  # layer and a game's own configuration name a physical input without
11
11
  # touching RGame::Core, which they may not do:
12
12
  #
13
- # bindings = Controls::DEFAULT_KEYBOARD.merge(fire: Controls::KEY_J)
14
- # RGame::Core::Input.new(app, bindings: bindings)
13
+ # controls = RGame::Util::Controls
14
+ # map = RGame::Engine::InputMap.default.merge(
15
+ # fire: { buttons: [controls::KEY_SPACE, controls::PAD_A] }
16
+ # )
17
+ #
18
+ # This module is the **vocabulary** only. What an id *means* to a game — the
19
+ # binding table — is RGame::Engine::InputMap, one per player, because two
20
+ # players share a game's actions but not the buttons that trigger them.
15
21
  #
16
22
  # The same numbers exist as `#define`s in
17
23
  # ext/rgame_core/include/rgame/core.h, because the C engine and the
@@ -20,17 +26,118 @@ module RGame
20
26
  # by _Static_assert at compile time, and spec/rgame/util/controls_spec.rb
21
27
  # parses that header and compares every value here against it.
22
28
  module Controls
23
- # --- Keyboard. Values are SDL scancodes. ---
29
+ # --- Keyboard. Values are SDL scancodes, which name a physical *position*
30
+ # rather than a letter: KEY_A is the key marked A on a QWERTY board and Q
31
+ # on AZERTY. A game rebinding controls shows the player what their layout
32
+ # calls it; the engine only ever compares numbers. ---
33
+ #
34
+ # The set is what a Western keyboard can be relied on to have. No numpad
35
+ # (most laptops have none), no GUI/Windows/Command key, no print-screen
36
+ # cluster, and nothing whose position depends on the layout.
37
+
38
+ # Letters. Scancodes are physical *positions*, so KEY_A is the key marked A on
39
+ # a QWERTY board and Q on AZERTY.
40
+ KEY_A = 4
41
+ KEY_B = 5
42
+ KEY_C = 6
43
+ KEY_D = 7
44
+ KEY_E = 8
45
+ KEY_F = 9
46
+ KEY_G = 10
47
+ KEY_H = 11
48
+ KEY_I = 12
49
+ KEY_J = 13
50
+ KEY_K = 14
51
+ KEY_L = 15
52
+ KEY_M = 16
53
+ KEY_N = 17
54
+ KEY_O = 18
55
+ KEY_P = 19
56
+ KEY_Q = 20
57
+ KEY_R = 21
58
+ KEY_S = 22
59
+ KEY_T = 23
60
+ KEY_U = 24
61
+ KEY_V = 25
62
+ KEY_W = 26
63
+ KEY_X = 27
64
+ KEY_Y = 28
65
+ KEY_Z = 29
66
+
67
+ # Digits along the top row.
68
+ KEY_1 = 30
69
+ KEY_2 = 31
70
+ KEY_3 = 32
71
+ KEY_4 = 33
72
+ KEY_5 = 34
73
+ KEY_6 = 35
74
+ KEY_7 = 36
75
+ KEY_8 = 37
76
+ KEY_9 = 38
77
+ KEY_0 = 39
78
+
79
+ # Editing and whitespace.
24
80
  KEY_RETURN = 40
25
81
  KEY_ESCAPE = 41
82
+ KEY_BACKSPACE = 42
83
+ KEY_TAB = 43
26
84
  KEY_SPACE = 44
85
+
86
+ # Punctuation, by position on a US board.
87
+ KEY_MINUS = 45
88
+ KEY_EQUALS = 46
89
+ KEY_LEFTBRACKET = 47
90
+ KEY_RIGHTBRACKET = 48
91
+ KEY_BACKSLASH = 49
92
+ KEY_SEMICOLON = 51
93
+ KEY_APOSTROPHE = 52
94
+ KEY_GRAVE = 53
95
+ KEY_COMMA = 54
96
+ KEY_PERIOD = 55
97
+ KEY_SLASH = 56
98
+
99
+ # Function row and caps lock.
100
+ KEY_CAPSLOCK = 57
27
101
  KEY_F1 = 58
102
+ KEY_F2 = 59
103
+ KEY_F3 = 60
104
+ KEY_F4 = 61
105
+ KEY_F5 = 62
106
+ KEY_F6 = 63
107
+ KEY_F7 = 64
108
+ KEY_F8 = 65
109
+ KEY_F9 = 66
110
+ KEY_F10 = 67
111
+ KEY_F11 = 68
112
+ KEY_F12 = 69
113
+
114
+ # The navigation cluster.
115
+ KEY_INSERT = 73
116
+ KEY_HOME = 74
117
+ KEY_PAGEUP = 75
118
+ KEY_DELETE = 76
119
+ KEY_END = 77
120
+ KEY_PAGEDOWN = 78
121
+
122
+ # Arrows.
28
123
  KEY_RIGHT = 79
29
124
  KEY_LEFT = 80
30
125
  KEY_DOWN = 81
31
126
  KEY_UP = 82
32
127
 
33
- # --- Gamepad buttons. The gamepad range plus SDL's controller button. ---
128
+ # Modifiers. No GUI key: that is Windows on a PC and Command on a Mac, which
129
+ # is exactly the platform-specific territory this list stays out of.
130
+ KEY_LCTRL = 224
131
+ KEY_LSHIFT = 225
132
+ KEY_LALT = 226
133
+ KEY_RCTRL = 228
134
+ KEY_RSHIFT = 229
135
+ KEY_RALT = 230
136
+
137
+ # --- Gamepad buttons. The gamepad range plus SDL's own controller button
138
+ # number. The first fifteen are on every controller; MISC1, the paddles
139
+ # and TOUCHPAD are hardware the id space describes but most pads do not
140
+ # have, and read as never pressed on one that does not. ---
34
141
  PAD_A = 4096
35
142
  PAD_B = 4097
36
143
  PAD_X = 4098
@@ -46,6 +153,12 @@ module RGame
46
153
  PAD_DPAD_DOWN = 4108
47
154
  PAD_DPAD_LEFT = 4109
48
155
  PAD_DPAD_RIGHT = 4110
156
+ PAD_MISC1 = 4111
157
+ PAD_PADDLE1 = 4112
158
+ PAD_PADDLE2 = 4113
159
+ PAD_PADDLE3 = 4114
160
+ PAD_PADDLE4 = 4115
161
+ PAD_TOUCHPAD = 4116
49
162
 
50
163
  # --- Analog axes. Their own small space: they are float-valued and read
51
164
  # through a different call, so folding them into the button space would
@@ -65,43 +178,6 @@ module RGame
65
178
 
66
179
  # The device id for a player slot: gamepad(0) is the first controller.
67
180
  def self.gamepad(slot) = GAMEPAD_FIRST + slot
68
-
69
- # --- Default bindings ---
70
- #
71
- # Symbolic action => physical input. Games override these to rebind; they
72
- # are values, so a config screen can build its own table from the
73
- # constants above and hand it to the input layer.
74
- #
75
- # Two button tables rather than one, because the same action is a
76
- # different physical input per device class: :fire is the space bar on a
77
- # keyboard and the A button on a pad.
78
- DEFAULT_KEYBOARD = {
79
- left: KEY_LEFT,
80
- right: KEY_RIGHT,
81
- up: KEY_UP,
82
- down: KEY_DOWN,
83
- confirm: KEY_RETURN,
84
- fire: KEY_SPACE
85
- }.freeze
86
-
87
- DEFAULT_PAD = {
88
- left: PAD_DPAD_LEFT,
89
- right: PAD_DPAD_RIGHT,
90
- up: PAD_DPAD_UP,
91
- down: PAD_DPAD_DOWN,
92
- confirm: PAD_A,
93
- fire: PAD_A
94
- }.freeze
95
-
96
- # Analog axes exist only on pads, so there is one table.
97
- DEFAULT_AXES = {
98
- move_x: AXIS_LEFT_X,
99
- move_y: AXIS_LEFT_Y,
100
- aim_x: AXIS_RIGHT_X,
101
- aim_y: AXIS_RIGHT_Y,
102
- trigger_left: AXIS_TRIGGER_LEFT,
103
- trigger_right: AXIS_TRIGGER_RIGHT
104
- }.freeze
105
181
  end
106
182
  end
107
183
  end