@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.
- package/LICENSE +23 -0
- package/README.md +299 -0
- package/package.json +60 -0
- package/s/demo/main.bundle.ts +14 -0
- package/s/demo/main.css +57 -0
- package/s/index.html.ts +42 -0
- package/s/index.ts +16 -0
- package/s/nubs/lookpad/styles.ts +21 -0
- package/s/nubs/lookpad/utils/listeners.ts +53 -0
- package/s/nubs/lookpad/view.ts +32 -0
- package/s/nubs/stick/device.ts +30 -0
- package/s/nubs/stick/styles.ts +22 -0
- package/s/nubs/stick/utils/calculate_new_vector_from_pointer_position.ts +27 -0
- package/s/nubs/stick/utils/find_closest_point_on_circle.ts +15 -0
- package/s/nubs/stick/utils/make_pointer_listeners.ts +50 -0
- package/s/nubs/stick/utils/within_radius.ts +6 -0
- package/s/nubs/stick/view.ts +50 -0
- package/s/nubs/stick-graphic/styles.ts +38 -0
- package/s/nubs/stick-graphic/types/basis.ts +5 -0
- package/s/nubs/stick-graphic/utils/calculate_basis.ts +19 -0
- package/s/nubs/stick-graphic/utils/stick_vector_to_pixels.ts +13 -0
- package/s/nubs/stick-graphic/utils/transform.ts +10 -0
- package/s/nubs/stick-graphic/view.ts +43 -0
- package/s/nubs/virtual-gamepad/device.ts +25 -0
- package/s/nubs/virtual-gamepad/styles.css.ts +133 -0
- package/s/nubs/virtual-gamepad/utils/gamepad-inputs.ts +42 -0
- package/s/nubs/virtual-gamepad/utils/prevent-default-touch-shenanigans.ts +12 -0
- package/s/nubs/virtual-gamepad/utils/touch-tracking.ts +75 -0
- package/s/nubs/virtual-gamepad/view.ts +139 -0
- package/s/station/devices/gamepad.ts +81 -0
- package/s/station/devices/infra/device.ts +7 -0
- package/s/station/devices/infra/group.ts +17 -0
- package/s/station/devices/infra/sampler.ts +22 -0
- package/s/station/devices/keyboard.ts +53 -0
- package/s/station/devices/pointer.ts +95 -0
- package/s/station/parts/action.ts +26 -0
- package/s/station/parts/defaults.ts +28 -0
- package/s/station/parts/resolver.ts +73 -0
- package/s/station/parts/routines/aggregate_samples_into_map.ts +20 -0
- package/s/station/parts/routines/build_updatable_actions_structure.ts +29 -0
- package/s/station/parts/routines/lensing_algorithm.ts +74 -0
- package/s/station/parts/switchboard-bindings.ts +21 -0
- package/s/station/station.test.ts +86 -0
- package/s/station/station.ts +47 -0
- package/s/station/switchboard.ts +107 -0
- package/s/station/testing/testing.ts +47 -0
- package/s/station/types.ts +72 -0
- package/s/station/utils/is-pressed.ts +5 -0
- package/s/station/utils/modprefix.ts +16 -0
- package/s/station/utils/tmax.ts +7 -0
- package/s/station/utils/tmin.ts +7 -0
- package/s/tests.test.ts +8 -0
- package/s/utils/evergreen.ts +10 -0
- package/s/utils/gamepads.ts +41 -0
- package/s/utils/split-axis.ts +7 -0
- package/x/demo/main.bundle.d.ts +1 -0
- package/x/demo/main.bundle.js +11 -0
- package/x/demo/main.bundle.js.map +1 -0
- package/x/demo/main.bundle.min.js +139 -0
- package/x/demo/main.bundle.min.js.map +7 -0
- package/x/demo/main.css +57 -0
- package/x/index.d.ts +13 -0
- package/x/index.html +97 -0
- package/x/index.html.d.ts +2 -0
- package/x/index.html.js +37 -0
- package/x/index.html.js.map +1 -0
- package/x/index.js +14 -0
- package/x/index.js.map +1 -0
- package/x/nubs/lookpad/styles.d.ts +1 -0
- package/x/nubs/lookpad/styles.js +21 -0
- package/x/nubs/lookpad/styles.js.map +1 -0
- package/x/nubs/lookpad/utils/listeners.d.ts +19 -0
- package/x/nubs/lookpad/utils/listeners.js +37 -0
- package/x/nubs/lookpad/utils/listeners.js.map +1 -0
- package/x/nubs/lookpad/view.d.ts +1 -0
- package/x/nubs/lookpad/view.js +24 -0
- package/x/nubs/lookpad/view.js.map +1 -0
- package/x/nubs/stick/device.d.ts +15 -0
- package/x/nubs/stick/device.js +27 -0
- package/x/nubs/stick/device.js.map +1 -0
- package/x/nubs/stick/styles.d.ts +1 -0
- package/x/nubs/stick/styles.js +22 -0
- package/x/nubs/stick/styles.js.map +1 -0
- package/x/nubs/stick/utils/calculate_new_vector_from_pointer_position.d.ts +3 -0
- package/x/nubs/stick/utils/calculate_new_vector_from_pointer_position.js +16 -0
- package/x/nubs/stick/utils/calculate_new_vector_from_pointer_position.js.map +1 -0
- package/x/nubs/stick/utils/find_closest_point_on_circle.d.ts +2 -0
- package/x/nubs/stick/utils/find_closest_point_on_circle.js +6 -0
- package/x/nubs/stick/utils/find_closest_point_on_circle.js.map +1 -0
- package/x/nubs/stick/utils/make_pointer_listeners.d.ts +16 -0
- package/x/nubs/stick/utils/make_pointer_listeners.js +34 -0
- package/x/nubs/stick/utils/make_pointer_listeners.js.map +1 -0
- package/x/nubs/stick/utils/within_radius.d.ts +2 -0
- package/x/nubs/stick/utils/within_radius.js +4 -0
- package/x/nubs/stick/utils/within_radius.js.map +1 -0
- package/x/nubs/stick/view.d.ts +2 -0
- package/x/nubs/stick/view.js +38 -0
- package/x/nubs/stick/view.js.map +1 -0
- package/x/nubs/stick-graphic/styles.d.ts +1 -0
- package/x/nubs/stick-graphic/styles.js +38 -0
- package/x/nubs/stick-graphic/styles.js.map +1 -0
- package/x/nubs/stick-graphic/types/basis.d.ts +4 -0
- package/x/nubs/stick-graphic/types/basis.js +2 -0
- package/x/nubs/stick-graphic/types/basis.js.map +1 -0
- package/x/nubs/stick-graphic/utils/calculate_basis.d.ts +2 -0
- package/x/nubs/stick-graphic/utils/calculate_basis.js +10 -0
- package/x/nubs/stick-graphic/utils/calculate_basis.js.map +1 -0
- package/x/nubs/stick-graphic/utils/stick_vector_to_pixels.d.ts +2 -0
- package/x/nubs/stick-graphic/utils/stick_vector_to_pixels.js +7 -0
- package/x/nubs/stick-graphic/utils/stick_vector_to_pixels.js.map +1 -0
- package/x/nubs/stick-graphic/utils/transform.d.ts +2 -0
- package/x/nubs/stick-graphic/utils/transform.js +7 -0
- package/x/nubs/stick-graphic/utils/transform.js.map +1 -0
- package/x/nubs/stick-graphic/view.d.ts +3 -0
- package/x/nubs/stick-graphic/view.js +30 -0
- package/x/nubs/stick-graphic/view.js.map +1 -0
- package/x/nubs/virtual-gamepad/device.d.ts +7 -0
- package/x/nubs/virtual-gamepad/device.js +20 -0
- package/x/nubs/virtual-gamepad/device.js.map +1 -0
- package/x/nubs/virtual-gamepad/styles.css.d.ts +2 -0
- package/x/nubs/virtual-gamepad/styles.css.js +133 -0
- package/x/nubs/virtual-gamepad/styles.css.js.map +1 -0
- package/x/nubs/virtual-gamepad/utils/gamepad-inputs.d.ts +29 -0
- package/x/nubs/virtual-gamepad/utils/gamepad-inputs.js +31 -0
- package/x/nubs/virtual-gamepad/utils/gamepad-inputs.js.map +1 -0
- package/x/nubs/virtual-gamepad/utils/prevent-default-touch-shenanigans.d.ts +1 -0
- package/x/nubs/virtual-gamepad/utils/prevent-default-touch-shenanigans.js +9 -0
- package/x/nubs/virtual-gamepad/utils/prevent-default-touch-shenanigans.js.map +1 -0
- package/x/nubs/virtual-gamepad/utils/touch-tracking.d.ts +6 -0
- package/x/nubs/virtual-gamepad/utils/touch-tracking.js +55 -0
- package/x/nubs/virtual-gamepad/utils/touch-tracking.js.map +1 -0
- package/x/nubs/virtual-gamepad/view.d.ts +2 -0
- package/x/nubs/virtual-gamepad/view.js +120 -0
- package/x/nubs/virtual-gamepad/view.js.map +1 -0
- package/x/station/devices/gamepad.d.ts +10 -0
- package/x/station/devices/gamepad.js +70 -0
- package/x/station/devices/gamepad.js.map +1 -0
- package/x/station/devices/infra/device.d.ts +4 -0
- package/x/station/devices/infra/device.js +3 -0
- package/x/station/devices/infra/device.js.map +1 -0
- package/x/station/devices/infra/group.d.ts +7 -0
- package/x/station/devices/infra/group.js +13 -0
- package/x/station/devices/infra/group.js.map +1 -0
- package/x/station/devices/infra/sampler.d.ts +8 -0
- package/x/station/devices/infra/sampler.js +17 -0
- package/x/station/devices/infra/sampler.js.map +1 -0
- package/x/station/devices/keyboard.d.ts +9 -0
- package/x/station/devices/keyboard.js +42 -0
- package/x/station/devices/keyboard.js.map +1 -0
- package/x/station/devices/pointer.d.ts +11 -0
- package/x/station/devices/pointer.js +79 -0
- package/x/station/devices/pointer.js.map +1 -0
- package/x/station/parts/action.d.ts +12 -0
- package/x/station/parts/action.js +23 -0
- package/x/station/parts/action.js.map +1 -0
- package/x/station/parts/defaults.d.ts +5 -0
- package/x/station/parts/defaults.js +22 -0
- package/x/station/parts/defaults.js.map +1 -0
- package/x/station/parts/resolver.d.ts +10 -0
- package/x/station/parts/resolver.js +63 -0
- package/x/station/parts/resolver.js.map +1 -0
- package/x/station/parts/routines/aggregate_samples_into_map.d.ts +3 -0
- package/x/station/parts/routines/aggregate_samples_into_map.js +11 -0
- package/x/station/parts/routines/aggregate_samples_into_map.js.map +1 -0
- package/x/station/parts/routines/build_updatable_actions_structure.d.ts +5 -0
- package/x/station/parts/routines/build_updatable_actions_structure.js +18 -0
- package/x/station/parts/routines/build_updatable_actions_structure.js.map +1 -0
- package/x/station/parts/routines/lensing_algorithm.d.ts +2 -0
- package/x/station/parts/routines/lensing_algorithm.js +42 -0
- package/x/station/parts/routines/lensing_algorithm.js.map +1 -0
- package/x/station/parts/switchboard-bindings.d.ts +2 -0
- package/x/station/parts/switchboard-bindings.js +19 -0
- package/x/station/parts/switchboard-bindings.js.map +1 -0
- package/x/station/station.d.ts +15 -0
- package/x/station/station.js +35 -0
- package/x/station/station.js.map +1 -0
- package/x/station/station.test.d.ts +11 -0
- package/x/station/station.test.js +80 -0
- package/x/station/station.test.js.map +1 -0
- package/x/station/switchboard.d.ts +30 -0
- package/x/station/switchboard.js +90 -0
- package/x/station/switchboard.js.map +1 -0
- package/x/station/testing/testing.d.ts +58 -0
- package/x/station/testing/testing.js +39 -0
- package/x/station/testing/testing.js.map +1 -0
- package/x/station/types.d.ts +56 -0
- package/x/station/types.js +5 -0
- package/x/station/types.js.map +1 -0
- package/x/station/utils/is-pressed.d.ts +1 -0
- package/x/station/utils/is-pressed.js +4 -0
- package/x/station/utils/is-pressed.js.map +1 -0
- package/x/station/utils/modprefix.d.ts +1 -0
- package/x/station/utils/modprefix.js +16 -0
- package/x/station/utils/modprefix.js.map +1 -0
- package/x/station/utils/tmax.d.ts +1 -0
- package/x/station/utils/tmax.js +6 -0
- package/x/station/utils/tmax.js.map +1 -0
- package/x/station/utils/tmin.d.ts +1 -0
- package/x/station/utils/tmin.js +6 -0
- package/x/station/utils/tmin.js.map +1 -0
- package/x/tests.test.d.ts +1 -0
- package/x/tests.test.js +6 -0
- package/x/tests.test.js.map +1 -0
- package/x/utils/evergreen.d.ts +1 -0
- package/x/utils/evergreen.js +10 -0
- package/x/utils/evergreen.js.map +1 -0
- package/x/utils/gamepads.d.ts +14 -0
- package/x/utils/gamepads.js +40 -0
- package/x/utils/gamepads.js.map +1 -0
- package/x/utils/split-axis.d.ts +1 -0
- package/x/utils/split-axis.js +6 -0
- 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
|
+
|
package/s/demo/main.css
ADDED
|
@@ -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
|
+
|
package/s/index.html.ts
ADDED
|
@@ -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
|
+
`
|