@benev/tact 0.1.0-1

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 (212) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +299 -0
  3. package/package.json +60 -0
  4. package/s/demo/main.bundle.ts +14 -0
  5. package/s/demo/main.css +57 -0
  6. package/s/index.html.ts +42 -0
  7. package/s/index.ts +16 -0
  8. package/s/nubs/lookpad/styles.ts +21 -0
  9. package/s/nubs/lookpad/utils/listeners.ts +53 -0
  10. package/s/nubs/lookpad/view.ts +32 -0
  11. package/s/nubs/stick/device.ts +30 -0
  12. package/s/nubs/stick/styles.ts +22 -0
  13. package/s/nubs/stick/utils/calculate_new_vector_from_pointer_position.ts +27 -0
  14. package/s/nubs/stick/utils/find_closest_point_on_circle.ts +15 -0
  15. package/s/nubs/stick/utils/make_pointer_listeners.ts +50 -0
  16. package/s/nubs/stick/utils/within_radius.ts +6 -0
  17. package/s/nubs/stick/view.ts +50 -0
  18. package/s/nubs/stick-graphic/styles.ts +38 -0
  19. package/s/nubs/stick-graphic/types/basis.ts +5 -0
  20. package/s/nubs/stick-graphic/utils/calculate_basis.ts +19 -0
  21. package/s/nubs/stick-graphic/utils/stick_vector_to_pixels.ts +13 -0
  22. package/s/nubs/stick-graphic/utils/transform.ts +10 -0
  23. package/s/nubs/stick-graphic/view.ts +43 -0
  24. package/s/nubs/virtual-gamepad/device.ts +25 -0
  25. package/s/nubs/virtual-gamepad/styles.css.ts +133 -0
  26. package/s/nubs/virtual-gamepad/utils/gamepad-inputs.ts +42 -0
  27. package/s/nubs/virtual-gamepad/utils/prevent-default-touch-shenanigans.ts +12 -0
  28. package/s/nubs/virtual-gamepad/utils/touch-tracking.ts +75 -0
  29. package/s/nubs/virtual-gamepad/view.ts +139 -0
  30. package/s/station/devices/gamepad.ts +81 -0
  31. package/s/station/devices/infra/device.ts +7 -0
  32. package/s/station/devices/infra/group.ts +17 -0
  33. package/s/station/devices/infra/sampler.ts +22 -0
  34. package/s/station/devices/keyboard.ts +53 -0
  35. package/s/station/devices/pointer.ts +95 -0
  36. package/s/station/parts/action.ts +26 -0
  37. package/s/station/parts/defaults.ts +28 -0
  38. package/s/station/parts/resolver.ts +73 -0
  39. package/s/station/parts/routines/aggregate_samples_into_map.ts +20 -0
  40. package/s/station/parts/routines/build_updatable_actions_structure.ts +29 -0
  41. package/s/station/parts/routines/lensing_algorithm.ts +74 -0
  42. package/s/station/parts/switchboard-bindings.ts +21 -0
  43. package/s/station/station.test.ts +86 -0
  44. package/s/station/station.ts +47 -0
  45. package/s/station/switchboard.ts +107 -0
  46. package/s/station/testing/testing.ts +47 -0
  47. package/s/station/types.ts +72 -0
  48. package/s/station/utils/is-pressed.ts +5 -0
  49. package/s/station/utils/modprefix.ts +16 -0
  50. package/s/station/utils/tmax.ts +7 -0
  51. package/s/station/utils/tmin.ts +7 -0
  52. package/s/tests.test.ts +8 -0
  53. package/s/utils/evergreen.ts +10 -0
  54. package/s/utils/gamepads.ts +41 -0
  55. package/s/utils/split-axis.ts +7 -0
  56. package/x/demo/main.bundle.d.ts +1 -0
  57. package/x/demo/main.bundle.js +11 -0
  58. package/x/demo/main.bundle.js.map +1 -0
  59. package/x/demo/main.bundle.min.js +139 -0
  60. package/x/demo/main.bundle.min.js.map +7 -0
  61. package/x/demo/main.css +57 -0
  62. package/x/index.d.ts +13 -0
  63. package/x/index.html +97 -0
  64. package/x/index.html.d.ts +2 -0
  65. package/x/index.html.js +37 -0
  66. package/x/index.html.js.map +1 -0
  67. package/x/index.js +14 -0
  68. package/x/index.js.map +1 -0
  69. package/x/nubs/lookpad/styles.d.ts +1 -0
  70. package/x/nubs/lookpad/styles.js +21 -0
  71. package/x/nubs/lookpad/styles.js.map +1 -0
  72. package/x/nubs/lookpad/utils/listeners.d.ts +19 -0
  73. package/x/nubs/lookpad/utils/listeners.js +37 -0
  74. package/x/nubs/lookpad/utils/listeners.js.map +1 -0
  75. package/x/nubs/lookpad/view.d.ts +1 -0
  76. package/x/nubs/lookpad/view.js +24 -0
  77. package/x/nubs/lookpad/view.js.map +1 -0
  78. package/x/nubs/stick/device.d.ts +15 -0
  79. package/x/nubs/stick/device.js +27 -0
  80. package/x/nubs/stick/device.js.map +1 -0
  81. package/x/nubs/stick/styles.d.ts +1 -0
  82. package/x/nubs/stick/styles.js +22 -0
  83. package/x/nubs/stick/styles.js.map +1 -0
  84. package/x/nubs/stick/utils/calculate_new_vector_from_pointer_position.d.ts +3 -0
  85. package/x/nubs/stick/utils/calculate_new_vector_from_pointer_position.js +16 -0
  86. package/x/nubs/stick/utils/calculate_new_vector_from_pointer_position.js.map +1 -0
  87. package/x/nubs/stick/utils/find_closest_point_on_circle.d.ts +2 -0
  88. package/x/nubs/stick/utils/find_closest_point_on_circle.js +6 -0
  89. package/x/nubs/stick/utils/find_closest_point_on_circle.js.map +1 -0
  90. package/x/nubs/stick/utils/make_pointer_listeners.d.ts +16 -0
  91. package/x/nubs/stick/utils/make_pointer_listeners.js +34 -0
  92. package/x/nubs/stick/utils/make_pointer_listeners.js.map +1 -0
  93. package/x/nubs/stick/utils/within_radius.d.ts +2 -0
  94. package/x/nubs/stick/utils/within_radius.js +4 -0
  95. package/x/nubs/stick/utils/within_radius.js.map +1 -0
  96. package/x/nubs/stick/view.d.ts +2 -0
  97. package/x/nubs/stick/view.js +38 -0
  98. package/x/nubs/stick/view.js.map +1 -0
  99. package/x/nubs/stick-graphic/styles.d.ts +1 -0
  100. package/x/nubs/stick-graphic/styles.js +38 -0
  101. package/x/nubs/stick-graphic/styles.js.map +1 -0
  102. package/x/nubs/stick-graphic/types/basis.d.ts +4 -0
  103. package/x/nubs/stick-graphic/types/basis.js +2 -0
  104. package/x/nubs/stick-graphic/types/basis.js.map +1 -0
  105. package/x/nubs/stick-graphic/utils/calculate_basis.d.ts +2 -0
  106. package/x/nubs/stick-graphic/utils/calculate_basis.js +10 -0
  107. package/x/nubs/stick-graphic/utils/calculate_basis.js.map +1 -0
  108. package/x/nubs/stick-graphic/utils/stick_vector_to_pixels.d.ts +2 -0
  109. package/x/nubs/stick-graphic/utils/stick_vector_to_pixels.js +7 -0
  110. package/x/nubs/stick-graphic/utils/stick_vector_to_pixels.js.map +1 -0
  111. package/x/nubs/stick-graphic/utils/transform.d.ts +2 -0
  112. package/x/nubs/stick-graphic/utils/transform.js +7 -0
  113. package/x/nubs/stick-graphic/utils/transform.js.map +1 -0
  114. package/x/nubs/stick-graphic/view.d.ts +3 -0
  115. package/x/nubs/stick-graphic/view.js +30 -0
  116. package/x/nubs/stick-graphic/view.js.map +1 -0
  117. package/x/nubs/virtual-gamepad/device.d.ts +7 -0
  118. package/x/nubs/virtual-gamepad/device.js +20 -0
  119. package/x/nubs/virtual-gamepad/device.js.map +1 -0
  120. package/x/nubs/virtual-gamepad/styles.css.d.ts +2 -0
  121. package/x/nubs/virtual-gamepad/styles.css.js +133 -0
  122. package/x/nubs/virtual-gamepad/styles.css.js.map +1 -0
  123. package/x/nubs/virtual-gamepad/utils/gamepad-inputs.d.ts +29 -0
  124. package/x/nubs/virtual-gamepad/utils/gamepad-inputs.js +31 -0
  125. package/x/nubs/virtual-gamepad/utils/gamepad-inputs.js.map +1 -0
  126. package/x/nubs/virtual-gamepad/utils/prevent-default-touch-shenanigans.d.ts +1 -0
  127. package/x/nubs/virtual-gamepad/utils/prevent-default-touch-shenanigans.js +9 -0
  128. package/x/nubs/virtual-gamepad/utils/prevent-default-touch-shenanigans.js.map +1 -0
  129. package/x/nubs/virtual-gamepad/utils/touch-tracking.d.ts +6 -0
  130. package/x/nubs/virtual-gamepad/utils/touch-tracking.js +55 -0
  131. package/x/nubs/virtual-gamepad/utils/touch-tracking.js.map +1 -0
  132. package/x/nubs/virtual-gamepad/view.d.ts +2 -0
  133. package/x/nubs/virtual-gamepad/view.js +120 -0
  134. package/x/nubs/virtual-gamepad/view.js.map +1 -0
  135. package/x/station/devices/gamepad.d.ts +10 -0
  136. package/x/station/devices/gamepad.js +70 -0
  137. package/x/station/devices/gamepad.js.map +1 -0
  138. package/x/station/devices/infra/device.d.ts +4 -0
  139. package/x/station/devices/infra/device.js +3 -0
  140. package/x/station/devices/infra/device.js.map +1 -0
  141. package/x/station/devices/infra/group.d.ts +7 -0
  142. package/x/station/devices/infra/group.js +13 -0
  143. package/x/station/devices/infra/group.js.map +1 -0
  144. package/x/station/devices/infra/sampler.d.ts +8 -0
  145. package/x/station/devices/infra/sampler.js +17 -0
  146. package/x/station/devices/infra/sampler.js.map +1 -0
  147. package/x/station/devices/keyboard.d.ts +9 -0
  148. package/x/station/devices/keyboard.js +42 -0
  149. package/x/station/devices/keyboard.js.map +1 -0
  150. package/x/station/devices/pointer.d.ts +11 -0
  151. package/x/station/devices/pointer.js +79 -0
  152. package/x/station/devices/pointer.js.map +1 -0
  153. package/x/station/parts/action.d.ts +12 -0
  154. package/x/station/parts/action.js +23 -0
  155. package/x/station/parts/action.js.map +1 -0
  156. package/x/station/parts/defaults.d.ts +5 -0
  157. package/x/station/parts/defaults.js +22 -0
  158. package/x/station/parts/defaults.js.map +1 -0
  159. package/x/station/parts/resolver.d.ts +10 -0
  160. package/x/station/parts/resolver.js +63 -0
  161. package/x/station/parts/resolver.js.map +1 -0
  162. package/x/station/parts/routines/aggregate_samples_into_map.d.ts +3 -0
  163. package/x/station/parts/routines/aggregate_samples_into_map.js +11 -0
  164. package/x/station/parts/routines/aggregate_samples_into_map.js.map +1 -0
  165. package/x/station/parts/routines/build_updatable_actions_structure.d.ts +5 -0
  166. package/x/station/parts/routines/build_updatable_actions_structure.js +18 -0
  167. package/x/station/parts/routines/build_updatable_actions_structure.js.map +1 -0
  168. package/x/station/parts/routines/lensing_algorithm.d.ts +2 -0
  169. package/x/station/parts/routines/lensing_algorithm.js +42 -0
  170. package/x/station/parts/routines/lensing_algorithm.js.map +1 -0
  171. package/x/station/parts/switchboard-bindings.d.ts +2 -0
  172. package/x/station/parts/switchboard-bindings.js +19 -0
  173. package/x/station/parts/switchboard-bindings.js.map +1 -0
  174. package/x/station/station.d.ts +15 -0
  175. package/x/station/station.js +35 -0
  176. package/x/station/station.js.map +1 -0
  177. package/x/station/station.test.d.ts +11 -0
  178. package/x/station/station.test.js +80 -0
  179. package/x/station/station.test.js.map +1 -0
  180. package/x/station/switchboard.d.ts +30 -0
  181. package/x/station/switchboard.js +90 -0
  182. package/x/station/switchboard.js.map +1 -0
  183. package/x/station/testing/testing.d.ts +58 -0
  184. package/x/station/testing/testing.js +39 -0
  185. package/x/station/testing/testing.js.map +1 -0
  186. package/x/station/types.d.ts +56 -0
  187. package/x/station/types.js +5 -0
  188. package/x/station/types.js.map +1 -0
  189. package/x/station/utils/is-pressed.d.ts +1 -0
  190. package/x/station/utils/is-pressed.js +4 -0
  191. package/x/station/utils/is-pressed.js.map +1 -0
  192. package/x/station/utils/modprefix.d.ts +1 -0
  193. package/x/station/utils/modprefix.js +16 -0
  194. package/x/station/utils/modprefix.js.map +1 -0
  195. package/x/station/utils/tmax.d.ts +1 -0
  196. package/x/station/utils/tmax.js +6 -0
  197. package/x/station/utils/tmax.js.map +1 -0
  198. package/x/station/utils/tmin.d.ts +1 -0
  199. package/x/station/utils/tmin.js +6 -0
  200. package/x/station/utils/tmin.js.map +1 -0
  201. package/x/tests.test.d.ts +1 -0
  202. package/x/tests.test.js +6 -0
  203. package/x/tests.test.js.map +1 -0
  204. package/x/utils/evergreen.d.ts +1 -0
  205. package/x/utils/evergreen.js +10 -0
  206. package/x/utils/evergreen.js.map +1 -0
  207. package/x/utils/gamepads.d.ts +14 -0
  208. package/x/utils/gamepads.js +40 -0
  209. package/x/utils/gamepads.js.map +1 -0
  210. package/x/utils/split-axis.d.ts +1 -0
  211. package/x/utils/split-axis.js +6 -0
  212. package/x/utils/split-axis.js.map +1 -0
package/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+
2
+ MIT License
3
+
4
+ Copyright (c) 2025 Chase Moskal
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
23
+
package/README.md ADDED
@@ -0,0 +1,299 @@
1
+
2
+ # @benev/tact
3
+ > user input keybindings and gamepad support for web games
4
+
5
+ ```ts
6
+ npm install @benev/tact
7
+ ```
8
+
9
+ - 🛰️ **devices** are for sampling user inputs
10
+ - 🔗 **bindings** describe how samples influence actions
11
+ - 📡 **station** runs device samples through bindings, updating actions
12
+ - 🔌 **switchboard** assigns devices to stations (multi-gamepad couch co-op!)
13
+ - 🔘 **nubs** is mobile ui virtual gamepad stuff
14
+
15
+ <br/><br/>
16
+
17
+ ## 🍋 tact devices
18
+ > produces user input "samples"
19
+
20
+ ### 🥝 polling is good, actually
21
+ - tact operates on the basis of *polling*
22
+ - *"but polling is bad"* says you — but no — you're wrong — polling is unironically *based,* and you *should* do it
23
+ - in a game, we want to be processing our inputs *every frame*
24
+ - the gift of polling is total control over *when* inputs are processed
25
+ - i will elaborate no further 🗿
26
+
27
+ ### 🥝 basically how a device works
28
+ - make a keyboard device
29
+ ```ts
30
+ import {KeyboardDevice} from "@benev/tact"
31
+
32
+ const device = new KeyboardDevice(window)
33
+ ```
34
+ - take samples each frame
35
+ ```ts
36
+ const samples = device.takeSamples()
37
+ // [
38
+ // ["KeyA", 1],
39
+ // ["Space", 0]
40
+ // ]
41
+ ```
42
+ - dispose the device when you're done with it
43
+ ```ts
44
+ device.dispose()
45
+ ```
46
+
47
+ ### 🥝 samples explained
48
+ - a sample is a raw input of type `[code: string, value: number]`
49
+ - a sample has a `code` string
50
+ - it's either a [standard keycode](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_code_values), like `KeyA`
51
+ - or it's something we made up, like `pointer.button.left` or `gamepad.trigger.right`
52
+ - a sample has a `value` number
53
+ - `0` means *"nothing is going on"*
54
+ - `1` means *"pressed"*
55
+ - we don't like negative numbers
56
+ - values between `0` and `1`, like `0.123`, are how triggers and thumbsticks express themselves
57
+ - sometimes we use numbers greater then `1`, like for dots of pointer movement like in `pointer.move.up`
58
+ - don't worry about sensitivity, deadzones, values like `0.00001` — actions will account for all that using bindings later on
59
+
60
+ ### 🥝 sample code modprefixes
61
+ - here at tact, we have this nifty `modprefix` convention
62
+ - consider a keycode like `KeyA`
63
+ - `ctrl-KeyA` means the "ctrl" modifier was held
64
+ - `alt-KeyA` means the "alt" modifier was held
65
+ - `meta-KeyA` means the "meta" modifier was held (command or windows keys)
66
+ - `shift-KeyA` means the "shift" modifier was held
67
+ - `ctrl-alt-meta-shift-KeyA` they can stack, but always in this order (the word `cams` helps remind me the valid order)
68
+ - `x-KeyA` means *no* modifier was held (exclusive)
69
+ - `KeyA` doesn't care if any modifiers were held or not
70
+
71
+ ### 🥝 sample code reference
72
+ - **KeyboardDevice**
73
+ - any [standard keycode](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_code_values)
74
+ - `KeyA`
75
+ - `Space`
76
+ - `Digit2`
77
+ - etc
78
+ - plus the modprefixes
79
+ - `ctrl-KeyA`
80
+ - `alt-shift-Space`
81
+ - `x-Digit2`
82
+ - etc
83
+ - **PointerDevice**
84
+ - mouse buttons (plus modprefixes)
85
+ - `pointer.button.left`
86
+ - `pointer.button.right`
87
+ - `pointer.button.middle`
88
+ - `pointer.button.4`
89
+ - `pointer.button.5`
90
+ - mouse wheel (plus modprefixes)
91
+ - `pointer.wheel.up`
92
+ - `pointer.wheel.down`
93
+ - `pointer.wheel.left`
94
+ - `pointer.wheel.right`
95
+ - mouse movements
96
+ - `pointer.move.up`
97
+ - `pointer.move.down`
98
+ - `pointer.move.left`
99
+ - `pointer.move.right`
100
+ - **GamepadDevice**
101
+ - gamepad buttons
102
+ - `gamepad.a`
103
+ - `gamepad.b`
104
+ - `gamepad.x`
105
+ - `gamepad.y`
106
+ - `gamepad.bumper.left`
107
+ - `gamepad.bumper.right`
108
+ - `gamepad.trigger.left`
109
+ - `gamepad.trigger.right`
110
+ - `gamepad.alpha`
111
+ - `gamepad.beta`
112
+ - `gamepad.stick.left.click`
113
+ - `gamepad.stick.right.click`
114
+ - `gamepad.up`
115
+ - `gamepad.down`
116
+ - `gamepad.left`
117
+ - `gamepad.right`
118
+ - `gamepad.gamma`
119
+ - gamepad sticks
120
+ - `gamepad.stick.left.up`
121
+ - `gamepad.stick.left.down`
122
+ - `gamepad.stick.left.left`
123
+ - `gamepad.stick.left.right`
124
+ - `gamepad.stick.right.up`
125
+ - `gamepad.stick.right.down`
126
+ - `gamepad.stick.right.left`
127
+ - `gamepad.stick.right.right`
128
+
129
+ <br/><br/>
130
+
131
+ ## 🍋 tact bindings
132
+ > keybindings! they describe how actions interpret samples
133
+
134
+ ### 🥝 bindings example
135
+ - let's start with a small example:
136
+ ```ts
137
+ import {Bindings} from "@benev/tact"
138
+
139
+ const bindings = asBindings({
140
+ walking: {
141
+ forward: [{lenses: [{code: "KeyW"}]}],
142
+ jump: [{lenses: [{code: "Space"}]}],
143
+ },
144
+ gunning: {
145
+ shoot: [{lenses: [{code: "pointer.button.left"}]}],
146
+ },
147
+ })
148
+ ```
149
+ - `walking` and `gunning` are **modes**
150
+ - `forward`, `jump`, and `shoot` are **actions**
151
+ - data like `[{lenses: [{code: "KeyW"}]}]` are the rules for triggering the action.
152
+ - 🚨 TODO okay these rules are complex and i'm gonna rework them soon
153
+ - whole modes can be enabled or disabled
154
+
155
+ <br/><br/>
156
+
157
+ ## 🍋 tact station
158
+ > polling gives you actions
159
+
160
+ ### 🥝 creating a station
161
+ - create a station
162
+ ```ts
163
+ import {Station} from "@benev/tact"
164
+
165
+ const station = new Station(bindings)
166
+ .addModes("walking")
167
+ .addDevices(
168
+ new KeyboardDevice(window),
169
+ new PointerDevice(window),
170
+ )
171
+ ```
172
+
173
+ ### 🥝 how to use actions
174
+ - poll the station every frame
175
+ ```ts
176
+ station.poll(Date.now())
177
+ ```
178
+ - now you use the `actions`
179
+ ```ts
180
+ station.actions.walking.forward.value // 1
181
+ ```
182
+ - `walking` is a `mode`
183
+ - `forward` is an `action`
184
+ - `action.value` — current value
185
+ - `action.previous` — last frame's value
186
+ - `action.changed` — true if value and previous are different
187
+ - `action.pressed` — true if the value > 0
188
+ - `action.down` — true for one frame when the key goes from up to down
189
+ - `action.up` — true for one frame when the key goes from down to up
190
+ - `action.on(action => {})` — react to changes ([`@e280/stz`](https://github.com/e280/stz) sub fn)
191
+ - `action.onDown(action => {})` — react only on down ([`@e280/stz`](https://github.com/e280/stz) sub fn)
192
+
193
+ ### 🥝 more about station
194
+ - you can enable/disable modes like this (it's a set)
195
+ ```ts
196
+ station.modes.add("gunning")
197
+ // now the "gunning" actions can fire
198
+
199
+ station.modes.delete("walking")
200
+ // now the "walking" actions *cannot* fire
201
+ ```
202
+ - you can add/remove devices from the set any time
203
+ ```ts
204
+ station.devices.add(new GamepadDevice(pad))
205
+ ```
206
+ - you can update the bindings at runtime
207
+ ```ts
208
+ station.bindings = bindings2
209
+ ```
210
+
211
+ <br/><br/>
212
+
213
+ ## 🍋 tact switchboard
214
+ > multiple gamepads! couch co-op is so back
215
+
216
+ ### 🥝 feel the vibes
217
+ - you know the way old timey game consoles had controller ports? and then players could plug their controller into whatever port they wanted? and then you could unplug and replug your controller into a different port? yeah — that's what switchboard does.
218
+ - with the switchboard, think of the "stations" as controller ports, and the "devices" as player controllers.
219
+ - import stuff:
220
+ ```ts
221
+ import {
222
+ Switchboard, Station,
223
+ GroupDevice, GamepadDevice, KeyboardDevice, PointerDevice,
224
+ } from "@benev/tact"
225
+ ```
226
+
227
+ ### 🥝 create a switchboard with stations
228
+ - **adopt standard switchboard bindings**
229
+ ```ts
230
+ // transform your game's bindings into switchboard-friendly bindings
231
+ const sBindings = Switchboard.bindings(bindings)
232
+ ```
233
+ - this allows players to shimmy what station their controller controls
234
+ - gamepad: hold middle button and press bumpers
235
+ - keyboard: left bracket or right bracket
236
+ - **make switchboard with stations at the ready**
237
+ ```ts
238
+ const switchboard = new Switchboard([
239
+ new Station(sBindings),
240
+ new Station(sBindings),
241
+ new Station(sBindings),
242
+ new Station(sBindings),
243
+ ])
244
+ ```
245
+ - yes that's right — each player can have their own bindings 🤯
246
+
247
+ ### 🥝 connect player's devices to the switchboard
248
+ - **let's connect the keyboard/mouse player**
249
+ ```ts
250
+ switchboard.connect(
251
+ new GroupDevice(
252
+ new KeyboardDevice(window),
253
+ new PointerDevice(window),
254
+ )
255
+ )
256
+ ```
257
+ - the switchboard assumes a single device represents a single player, thus we can use a `GroupDevice` to combine multple devices into one
258
+ - **wire up gamepad auto connect/disconnect**
259
+ ```ts
260
+ GamepadDevice.on(device => switchboard.connect(device))
261
+ ```
262
+
263
+ ### 🥝 you're ready!
264
+ - **do your polling, interrogate those actions**
265
+ ```ts
266
+ const [p1, p2, p3, p4] = switchboard.stations
267
+
268
+ // poll them all
269
+ switchboard.poll(Date.now())
270
+
271
+ p1.actions.walking.jump.value // 1
272
+ p2.actions.walking.jump.value // 0
273
+ ```
274
+
275
+ ### 🥝 the more you know, about the switchboard
276
+ - `switchboard.stations` — direct access to the array of stations
277
+ - `switchboard.isActive(p1)` — check if there's a switchboard-connected-device assigned to this station
278
+ - `switchboard.shimmy(device, 1)` — shimmy a device forward one station
279
+ - `switchboard.shimmy(device, -1)` — shimmy a device backward one station
280
+ - `switchboard.connect(device, p4)` — connect-or-reassign a device to a specific station
281
+ - `switchboard.connect(device)` — connect a device to the next unassigned station (or the last station)
282
+ - `switchboard.disconnect(device)` — unassign a device and forget about it
283
+
284
+ <br/><br/>
285
+
286
+ ## 🍋 tact nubs
287
+ > mobile ui like virtual thumbsticks and buttons
288
+
289
+ ### 🥝 nub stick
290
+ > TODO lol need to write docs
291
+
292
+ ### 🥝 nub virtual gamepad
293
+ > TODO lol need to write docs
294
+
295
+ <br/><br/>
296
+
297
+ ## 🍋 tact is by https://benevolent.games/
298
+ > building the future of web games
299
+
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@benev/tact",
3
+ "version": "0.1.0-1",
4
+ "description": "keybindings and gamepad support for web games",
5
+ "license": "MIT",
6
+ "author": "Chase Moskal <chasemoskal@gmail.com>",
7
+ "type": "module",
8
+ "main": "./x/index.js",
9
+ "sideEffects": false,
10
+ "exports": {
11
+ ".": "./x/index.js"
12
+ },
13
+ "files": [
14
+ "x",
15
+ "s"
16
+ ],
17
+ "scripts": {
18
+ "build": "run-s _clean _tsc _ln _scute",
19
+ "_clean": "rm -rf x",
20
+ "_tsc": "tsc",
21
+ "_scute": "scute -v",
22
+ "start": "octo 'scute -vw' 'tsc -w' 'node --watch x/tests.test.js' 'http-server x -c-1'",
23
+ "_ln": "run-s _ln-s _ln-assets",
24
+ "_ln-s": "ln -s \"$(realpath s)\" x/s",
25
+ "_ln-assets": "ln -s \"$(realpath assets)\" x/assets",
26
+ "test": "node x/tests.test.js",
27
+ "test-debug": "node inspect x/tests.test.js",
28
+ "count": "find s -path '*/_archive' -prune -o -name '*.ts' -exec wc -l {} +"
29
+ },
30
+ "peerDependencies": {
31
+ "lit": "^3.3.1"
32
+ },
33
+ "dependencies": {
34
+ "@benev/math": "^0.2.0-1",
35
+ "@e280/sly": "^0.1.1",
36
+ "@e280/strata": "^0.1.0",
37
+ "@e280/stz": "^0.2.0-4"
38
+ },
39
+ "devDependencies": {
40
+ "@e280/science": "^0.1.1",
41
+ "@e280/scute": "^0.0.0",
42
+ "http-server": "^14.1.1",
43
+ "npm-run-all": "^4.1.5",
44
+ "typescript": "^5.9.2"
45
+ },
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "git+https://github.com/benevolent-games/tact.git"
49
+ },
50
+ "bugs": {
51
+ "url": "https://github.com/benevolent-games/tact/issues"
52
+ },
53
+ "homepage": "https://github.com/benevolent-games/tact#readme",
54
+ "keywords": [
55
+ "user input",
56
+ "keybinds",
57
+ "keybindings",
58
+ "gamepad"
59
+ ]
60
+ }
@@ -0,0 +1,14 @@
1
+
2
+ import {$, view} from "@e280/sly"
3
+ import {NubStick} from "../nubs/stick/view.js"
4
+ import {StickDevice} from "../nubs/stick/device.js"
5
+
6
+ $.register({
7
+ TactDemo: view.component(use => {
8
+ const stick = use.once(() => new StickDevice())
9
+ return NubStick(stick)
10
+ }),
11
+ })
12
+
13
+ console.log("tact")
14
+
@@ -0,0 +1,57 @@
1
+
2
+ @layer vars, core, basics, page;
3
+
4
+ @layer vars {
5
+ :root {
6
+ color-scheme: dark;
7
+ --link: cyan;
8
+ --bg: #181818;
9
+ --prime: #aaa;
10
+ }
11
+ }
12
+
13
+ @layer core {
14
+ * {
15
+ padding: 0;
16
+ margin: 0;
17
+ box-sizing: border-box;
18
+ }
19
+
20
+ ::-webkit-scrollbar { width: 4px; }
21
+ ::-webkit-scrollbar-track { background: transparent; }
22
+ ::-webkit-scrollbar-thumb { background: #333; border-radius: 1em; }
23
+ ::-webkit-scrollbar-thumb:hover { background: #444; }
24
+
25
+ a {
26
+ color: var(--link);
27
+ text-decoration: none;
28
+
29
+ &:visited {
30
+ color: color-mix(in srgb, var(--link), purple 30%);
31
+ }
32
+
33
+ &:hover {
34
+ color: color-mix(in srgb, var(--link), white 10%);
35
+ text-decoration: underline;
36
+ }
37
+
38
+ &:active {
39
+ color: color-mix(in srgb, var(--link), white 50%);
40
+ }
41
+ }
42
+ }
43
+
44
+ @layer basics {
45
+ html { height: 100%; }
46
+ body { min-height: 100%; }
47
+
48
+ html, body {
49
+ font-size: 21px;
50
+ color: var(--prime);
51
+ background: var(--bg);
52
+ }
53
+ }
54
+
55
+ @layer page {
56
+ }
57
+
@@ -0,0 +1,42 @@
1
+
2
+ import {ssg, html} from "@e280/scute"
3
+
4
+ const title = "@benev/tact"
5
+ const domain = "tact.benevolent.games"
6
+ const favicon = "/assets/b.png"
7
+ const description = "keybindings and gamepad support for web games"
8
+
9
+ export default ssg.page(import.meta.url, async orb => ({
10
+ title,
11
+ js: "demo/main.bundle.min.js",
12
+ css: "demo/main.css",
13
+ dark: true,
14
+ favicon,
15
+ head: html`
16
+ <meta data-version="${orb.packageVersion()}" />
17
+ `,
18
+
19
+ socialCard: {
20
+ title,
21
+ description,
22
+ themeColor: "#f2ea8e",
23
+ siteName: domain,
24
+ image: `https://${domain}${favicon}`,
25
+ },
26
+
27
+ body: html`
28
+ <header>
29
+ <h1>
30
+ <strong>@benev/tact</strong>
31
+ <span>v${orb.packageVersion()}</span>
32
+ </h1>
33
+ <div class=deets>
34
+ <a href="https://github.com/benevolent-games/tact">github</a>
35
+ <a href="https://benevolent.games/">benevolent.games</a>
36
+ </div>
37
+ </header>
38
+
39
+ <tact-demo></tact-demo>
40
+ `,
41
+ }))
42
+
package/s/index.ts ADDED
@@ -0,0 +1,16 @@
1
+
2
+ export * from "./station/devices/infra/device.js"
3
+ export * from "./station/devices/infra/group.js"
4
+ export * from "./station/devices/infra/sampler.js"
5
+ export * from "./station/devices/gamepad.js"
6
+ export * from "./station/devices/keyboard.js"
7
+ export * from "./station/devices/pointer.js"
8
+ export * from "./station/parts/action.js"
9
+ export * from "./station/utils/modprefix.js"
10
+ export * from "./station/station.js"
11
+ export * from "./station/switchboard.js"
12
+ export * from "./station/types.js"
13
+
14
+ export * from "./utils/gamepads.js"
15
+ export * from "./utils/split-axis.js"
16
+
@@ -0,0 +1,21 @@
1
+
2
+ import {css} from "lit"
3
+ export const styles = css`
4
+
5
+ :host {
6
+ display: block;
7
+ width: 10em;
8
+ height: 10em;
9
+
10
+ user-select: none;
11
+ border: 1px solid;
12
+
13
+ touch-action: none;
14
+ }
15
+
16
+ .pad {
17
+ width: 100%;
18
+ height: 100%;
19
+ }
20
+
21
+ `
@@ -0,0 +1,53 @@
1
+
2
+ export function lookpad_listeners({
3
+ onPointerDrag,
4
+ getPointerCaptureElement,
5
+ }: {
6
+ onPointerDrag: (event: PointerEvent) => void
7
+ getPointerCaptureElement: () => HTMLElement
8
+ }) {
9
+
10
+ let pointer_id: number | undefined
11
+
12
+ return {
13
+ pointerdown: {
14
+ options: undefined,
15
+ handleEvent: (event: PointerEvent) => {
16
+ event.preventDefault()
17
+
18
+ const element = getPointerCaptureElement()
19
+
20
+ if (pointer_id)
21
+ element.releasePointerCapture(pointer_id)
22
+
23
+ pointer_id = event.pointerId
24
+ element.setPointerCapture(pointer_id)
25
+ onPointerDrag(event)
26
+ },
27
+ },
28
+
29
+ pointermove: {
30
+ options: {passive: false},
31
+ handleEvent: (event: PointerEvent) => {
32
+ event.preventDefault()
33
+
34
+ if (event.pointerId === pointer_id)
35
+ onPointerDrag(event)
36
+ },
37
+ },
38
+
39
+ pointerup: {
40
+ options: undefined,
41
+ handleEvent: (event: PointerEvent) => {
42
+ event.preventDefault()
43
+
44
+ if (event.pointerId === pointer_id) {
45
+ getPointerCaptureElement().releasePointerCapture(pointer_id)
46
+ pointer_id = undefined
47
+ onPointerDrag(event)
48
+ }
49
+ },
50
+ },
51
+ }
52
+ }
53
+
@@ -0,0 +1,32 @@
1
+
2
+ import {html} from "lit"
3
+ import {view} from "@e280/sly"
4
+
5
+ import {styles} from "./styles.js"
6
+ import {lookpad_listeners} from "./utils/listeners.js"
7
+
8
+ export const NubLookpad = view(use => () => {
9
+ use.name("nub-lookpad")
10
+ use.styles(styles)
11
+
12
+ const pad = use.life(() => {
13
+ const pad = document.createElement("div")
14
+ pad.className = "pad"
15
+
16
+ const listeners = lookpad_listeners({
17
+ getPointerCaptureElement: () => pad,
18
+ onPointerDrag: () => {},
19
+ })
20
+
21
+ for (const [event, {handleEvent, options}] of Object.entries(listeners))
22
+ pad.addEventListener(event as any, handleEvent, options)
23
+
24
+ return [pad, () => {
25
+ for (const [event, {handleEvent}] of Object.entries(listeners))
26
+ pad.removeEventListener(event as any, handleEvent)
27
+ }]
28
+ })
29
+
30
+ return html`${pad}`
31
+ })
32
+
@@ -0,0 +1,30 @@
1
+
2
+ import {Vec2} from "@benev/math"
3
+ import {signal} from "@e280/strata"
4
+ import {Disposable} from "@e280/stz"
5
+ import {splitAxis} from "../../utils/split-axis.js"
6
+ import {SamplerDevice} from "../../station/devices/infra/sampler.js"
7
+
8
+ export class StickDevice extends SamplerDevice implements Disposable {
9
+ vector = signal(Vec2.zero())
10
+ dispose: () => void
11
+
12
+ constructor(public channel = "stick") {
13
+ super()
14
+ this.dispose = this.vector.on(() => {
15
+ const {up, down, left, right} = this.breakdown()
16
+ this.setSample(`${channel}.up`, up)
17
+ this.setSample(`${channel}.down`, down)
18
+ this.setSample(`${channel}.left`, left)
19
+ this.setSample(`${channel}.right`, right)
20
+ })
21
+ }
22
+
23
+ breakdown() {
24
+ const {x, y} = this.vector.get()
25
+ const [down, up] = splitAxis(y)
26
+ const [left, right] = splitAxis(x)
27
+ return {up, down, left, right}
28
+ }
29
+ }
30
+
@@ -0,0 +1,22 @@
1
+
2
+ import {css} from "lit"
3
+ export const styles = css`
4
+
5
+ :host {
6
+ display: block;
7
+ width: 8em;
8
+ aspect-ratio: 1 / 1;
9
+ touch-action: none;
10
+ }
11
+
12
+ .container {
13
+ width: 100%;
14
+ height: 100%;
15
+ }
16
+
17
+ [part="graphic"] {
18
+ width: 100%;
19
+ height: 100%;
20
+ }
21
+
22
+ `