@benev/tact 0.4.0-6 → 0.4.0-7
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/README.md +91 -22
- package/package.json +1 -1
- package/s/lib/input/evaluate/context.ts +5 -1
- package/s/lib/input/evaluate/evaluate.ts +27 -14
- package/s/lib/input/test.ts +41 -30
- package/s/lib/ui/deck/auto-gamepads.ts +1 -0
- package/s/lib/ui/deck/setup/deck.ts +33 -20
- package/s/lib/ui/deck/setup/tact.ts +20 -11
- package/s/lib/ui/deck/setup/types.ts +1 -0
- package/s/lib/ui/deck/setup/utils/broadcast-channel.ts +17 -0
- package/s/lib/ui/deck/setup/utils/kv.ts +20 -0
- package/s/lib/ui/deck/types.ts +1 -1
- package/s/lib/ui/views/customize/view.ts +3 -3
- package/x/demo/main.bundle.min.js +63 -63
- package/x/demo/main.bundle.min.js.map +4 -4
- package/x/gamepad/index.html +1 -1
- package/x/index.html +2 -2
- package/x/lib/input/evaluate/context.d.ts +3 -1
- package/x/lib/input/evaluate/context.js.map +1 -1
- package/x/lib/input/evaluate/evaluate.js +24 -11
- package/x/lib/input/evaluate/evaluate.js.map +1 -1
- package/x/lib/input/test.d.ts +3 -2
- package/x/lib/input/test.js +39 -22
- package/x/lib/input/test.js.map +1 -1
- package/x/lib/ui/deck/auto-gamepads.js +1 -0
- package/x/lib/ui/deck/auto-gamepads.js.map +1 -1
- package/x/lib/ui/deck/setup/deck.js +23 -13
- package/x/lib/ui/deck/setup/deck.js.map +1 -1
- package/x/lib/ui/deck/setup/tact.d.ts +2 -2
- package/x/lib/ui/deck/setup/tact.js +15 -8
- package/x/lib/ui/deck/setup/tact.js.map +1 -1
- package/x/lib/ui/deck/setup/types.d.ts +1 -0
- package/x/lib/ui/deck/setup/utils/broadcast-channel.d.ts +4 -0
- package/x/lib/ui/deck/setup/utils/broadcast-channel.js +10 -0
- package/x/lib/ui/deck/setup/utils/broadcast-channel.js.map +1 -0
- package/x/lib/ui/deck/setup/utils/kv.d.ts +5 -0
- package/x/lib/ui/deck/setup/utils/kv.js +12 -0
- package/x/lib/ui/deck/setup/utils/kv.js.map +1 -0
- package/x/lib/ui/deck/types.d.ts +1 -0
- package/x/lib/ui/views/customize/view.js +3 -3
- package/x/lib/ui/views/customize/view.js.map +1 -1
package/README.md
CHANGED
|
@@ -2,19 +2,21 @@
|
|
|
2
2
|
# 🎮 @benev/tact
|
|
3
3
|
> *from keypress to couch co-op.*
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
i made tact because i want to make indie web game development easier. user input seems like it should be easy, but doing it well is actually really complex. especially when you start thinking of multiplayer and multithreading. my goal with tact is to tame the complexity and give people something elegant and flexible that "just works".
|
|
5
|
+
### tact is a user-input toolkit for web games.
|
|
7
6
|
|
|
8
7
|
🌐 👉 ***https://tact.benev.gg/*** 👈 *try now*
|
|
9
|
-
|
|
10
8
|
🖱 keyboards, mice, gamepads.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
🎸 ready-to-rock drop-in user-interface.
|
|
9
|
+
🔥 powerful enough for multiplayer games, simple enough for any app.
|
|
10
|
+
🎸 users can customize keybinds with the ready-to-rock ui.
|
|
11
|
+
🧬 expressive bindings json dsl.
|
|
12
|
+
📡 binary streaming for multiplayer and multithreading.
|
|
16
13
|
✍️ handcrafted by a human being.
|
|
17
14
|
|
|
15
|
+
user input isn't as simple as you'd think.
|
|
16
|
+
after many design iterations, tact has evolved into an elegant system that handles hard problems for you like customizability, multiplayer, and multithreading. i'm trying to give you the *"it just works"* experience.
|
|
17
|
+
|
|
18
|
+
i care about the little things. all input types are boiled down to simple numbers, and packed up into efficient binary packets for streaming across networks and web workers. mouse movement precision and accuracy is holy, so there's careful design to make it resilient in the face of encoder/decoder sampling frequency mismatches. the bindings language is an infinitely capable recursive dsl so you can remap values from funky gamepads and make ridiculously complex logical hotkey combinations.
|
|
19
|
+
|
|
18
20
|
|
|
19
21
|
|
|
20
22
|

|
|
@@ -26,6 +28,7 @@ i made tact because i want to make indie web game development easier. user input
|
|
|
26
28
|
<br/>
|
|
27
29
|
|
|
28
30
|
## 🎮 let's play.
|
|
31
|
+
> *here's the fully-featured, batteries-included, ready-for-anything web game starter pack.*
|
|
29
32
|
|
|
30
33
|
- 🍏 **install tact into your typescript project.**
|
|
31
34
|
```sh
|
|
@@ -41,8 +44,7 @@ i made tact because i want to make indie web game development easier. user input
|
|
|
41
44
|
look: ["accumulate", "pointer.x"],
|
|
42
45
|
})
|
|
43
46
|
```
|
|
44
|
-
|
|
45
|
-
- 🍏 **let's have two presets available to the user. one for gamepads.**
|
|
47
|
+
- 🍏 **let's have two presets available to the user, with one for gamepads.**
|
|
46
48
|
```ts
|
|
47
49
|
import {asPresets} from "@benev/tact"
|
|
48
50
|
|
|
@@ -55,7 +57,7 @@ i made tact because i want to make indie web game development easier. user input
|
|
|
55
57
|
}},
|
|
56
58
|
})
|
|
57
59
|
```
|
|
58
|
-
-
|
|
60
|
+
- ⚙️ **setup tact.**
|
|
59
61
|
```ts
|
|
60
62
|
import {setupTact} from "@benev/tact"
|
|
61
63
|
|
|
@@ -64,15 +66,22 @@ i made tact because i want to make indie web game development easier. user input
|
|
|
64
66
|
tact.gamepads("gamepad")
|
|
65
67
|
tact.registerElements()
|
|
66
68
|
```
|
|
67
|
-
- 💡 *you can set `maxPlayers
|
|
69
|
+
- 💡 *you can set `{maxPlayers: 1}` for a single player game.*
|
|
70
|
+
- 💡 *you can pass `{namespace: "falrysk"}` if you need to run multiple games on one website.*
|
|
71
|
+
- 💡 *you can register elements manually like `customElements.define("tact-deck-falrysk", tact.elements.TactDeck)`.*
|
|
72
|
+
- 💡 *you can call `tact.dispose()` to release resources.*
|
|
68
73
|
- 🍏 **encoding, decoding, and your game logic.**
|
|
69
74
|
```ts
|
|
75
|
+
import {Collector} from "@benev/tact"
|
|
76
|
+
|
|
77
|
+
const collector = new Collector(bindings)
|
|
78
|
+
|
|
70
79
|
setInterval(() => {
|
|
71
|
-
const buffer = tact.
|
|
72
|
-
for (const {playerId, inputs} of
|
|
80
|
+
const buffer = tact.encodeAll()
|
|
81
|
+
for (const {playerId, inputs} of collector.decodeAll(buffer)) {
|
|
73
82
|
|
|
74
83
|
// your game logic can read these handy input values.
|
|
75
|
-
inputs.jump.value // values are numbers 0
|
|
84
|
+
inputs.jump.value // button-like values are numbers 0 to 1.
|
|
76
85
|
inputs.jump.was // previous value (to detect up/down).
|
|
77
86
|
inputs.jump.sum // sum of values reported since last frame.
|
|
78
87
|
|
|
@@ -81,19 +90,18 @@ i made tact because i want to make indie web game development easier. user input
|
|
|
81
90
|
}
|
|
82
91
|
}, 16.67)
|
|
83
92
|
```
|
|
84
|
-
- 💡 *
|
|
85
|
-
- 💡 *
|
|
93
|
+
- 💡 *you could stream the buffers across the network to a remote collector, maybe with [renraku](https://github.com/e280/renraku).*
|
|
94
|
+
- 💡 *you could have the tact deck on the clientside, and the collector on the serverside (or a web worker!).*
|
|
86
95
|
- 🍏 **put the `<tact-deck>` element in your html page.**
|
|
87
96
|
```html
|
|
88
97
|
<tact-deck></tact-deck>
|
|
89
98
|
```
|
|
90
|
-
- 💡 *
|
|
99
|
+
- 💡 *or just use the sub-panels directly, `<tact-controllers>` and `<tact-customize>`.*
|
|
91
100
|
- 💡 *you can customize the colors with css.*
|
|
92
101
|
```html
|
|
93
102
|
<style>
|
|
94
103
|
html {
|
|
95
104
|
--tact-prime: aqua;
|
|
96
|
-
--tact-bg: black;
|
|
97
105
|
}
|
|
98
106
|
</style>
|
|
99
107
|
```
|
|
@@ -104,9 +112,9 @@ i made tact because i want to make indie web game development easier. user input
|
|
|
104
112
|
<br/>
|
|
105
113
|
|
|
106
114
|
## 🎮 get down to brass tacks.
|
|
107
|
-
> *
|
|
115
|
+
> *for simpler apps, you can use tact's composable parts more directly.*
|
|
108
116
|
|
|
109
|
-
-
|
|
117
|
+
- ⚙️ **pov: your app isn't a game, no multiplayer, no gamepads.**
|
|
110
118
|
```ts
|
|
111
119
|
import {setupTact, InputDecoder} from "@benev/tact"
|
|
112
120
|
|
|
@@ -124,7 +132,7 @@ i made tact because i want to make indie web game development easier. user input
|
|
|
124
132
|
}, 16.67)
|
|
125
133
|
```
|
|
126
134
|
- 💡 *then the only ui you need is the `<tact-customize>` panel (also available as a sly view `CustomizeView`).*
|
|
127
|
-
-
|
|
135
|
+
- ⚙️ **pov: your app doesn't even want any ui, or persistence.**
|
|
128
136
|
```ts
|
|
129
137
|
import {Controller, InputDecoder, KeyboardSource, PointerSource, Sources} from "@benev/tact"
|
|
130
138
|
|
|
@@ -143,6 +151,67 @@ i made tact because i want to make indie web game development easier. user input
|
|
|
143
151
|
|
|
144
152
|
|
|
145
153
|
|
|
154
|
+
<br/>
|
|
155
|
+
|
|
156
|
+
## 🎮 bindings.
|
|
157
|
+
> *bindings are a little json programming language.*
|
|
158
|
+
|
|
159
|
+
- 🧬 **see [expression.ts](./s/lib/input/expression.ts) to get an idea of what's possible.**
|
|
160
|
+
- 🧬 **arbitrary nesting is valid.**
|
|
161
|
+
```ts
|
|
162
|
+
asBindings("KeyW")
|
|
163
|
+
|
|
164
|
+
inputs.value // 1
|
|
165
|
+
```
|
|
166
|
+
```ts
|
|
167
|
+
asBindings({
|
|
168
|
+
actions: {
|
|
169
|
+
special: {
|
|
170
|
+
castMagicSpell: ["*", ["mod", "shift", "alt"], "KeyR"],
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
inputs.actions.special.castMagicSpell.value // 1
|
|
176
|
+
```
|
|
177
|
+
- 🧬 **you can differentiate taps versus holds on the same key.**
|
|
178
|
+
```ts
|
|
179
|
+
asBindings({
|
|
180
|
+
|
|
181
|
+
// tapping E yoinks a weapon into your inventory.
|
|
182
|
+
grab: ["taps", 1, 250, "KeyE"],
|
|
183
|
+
|
|
184
|
+
// holding E equips the weapon as your primary.
|
|
185
|
+
equip: ["held", 250, "KeyE"],
|
|
186
|
+
})
|
|
187
|
+
```
|
|
188
|
+
- 🧬 **you can "accumulate" mouse inputs accurately,** to avoid losses in sampling frequency mismatches.
|
|
189
|
+
```ts
|
|
190
|
+
asBindings({
|
|
191
|
+
lookX: ["accumulate", "pointer.x"],
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
inputs.lookX.sum // use sum instead of value, to ensure you don't lose any pointer movement.
|
|
195
|
+
```
|
|
196
|
+
- 🧬 **gamepads thumbstick inputs are best done with some math.**
|
|
197
|
+
```ts
|
|
198
|
+
asBindings({
|
|
199
|
+
forward: ["*",
|
|
200
|
+
|
|
201
|
+
// user-customizable sensitivity, equivalent with pointer-pixels-per-second.
|
|
202
|
+
1234,
|
|
203
|
+
|
|
204
|
+
// delta time factor, to ensure sensitivity is framerate-independent.
|
|
205
|
+
["dt"],
|
|
206
|
+
|
|
207
|
+
// remapping the ranges because gamepads are naughty and often have jiggly deadzones.
|
|
208
|
+
["remap", -0.1, -0.9, 0, 1, "gamepad.axis.1"],
|
|
209
|
+
],
|
|
210
|
+
})
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
|
|
146
215
|
<br/><br/>
|
|
147
216
|
|
|
148
217
|
---
|
package/package.json
CHANGED
|
@@ -11,6 +11,10 @@ export class EvaluationContext {
|
|
|
11
11
|
previousValues = new Map<string, number>()
|
|
12
12
|
|
|
13
13
|
holding = new Map<object, {start: number}>()
|
|
14
|
-
tapping = new Map<object, {
|
|
14
|
+
tapping = new Map<object, {
|
|
15
|
+
wasDown: boolean
|
|
16
|
+
startedAt: number | null
|
|
17
|
+
count: number
|
|
18
|
+
}>()
|
|
15
19
|
}
|
|
16
20
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
|
|
2
2
|
import {remap} from "@benev/math"
|
|
3
|
-
import {
|
|
3
|
+
import {guarantee, isHappy, isNumber, isString} from "@e280/stz"
|
|
4
4
|
|
|
5
5
|
import {isDown} from "../utils/is-down.js"
|
|
6
6
|
import {EvaluationContext} from "./context.js"
|
|
@@ -181,27 +181,40 @@ export function evaluate(
|
|
|
181
181
|
|
|
182
182
|
case "taps": {
|
|
183
183
|
const [, taps, ms, subexpression] = expression
|
|
184
|
-
const
|
|
184
|
+
const down = isDown(e(subexpression))
|
|
185
185
|
const now = context.clock.time
|
|
186
186
|
|
|
187
|
-
const
|
|
188
|
-
|
|
187
|
+
const state = guarantee(context.tapping, expression, () => ({
|
|
188
|
+
wasDown: false,
|
|
189
|
+
startedAt: null as number | null,
|
|
190
|
+
count: 0,
|
|
189
191
|
}))
|
|
190
192
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
+
const pressed = down && !state.wasDown
|
|
194
|
+
const released = !down && state.wasDown
|
|
193
195
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
196
|
+
if (state.startedAt !== null && now - state.startedAt >= ms) {
|
|
197
|
+
state.startedAt = null
|
|
198
|
+
state.count = 0
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
let result = 0
|
|
198
202
|
|
|
199
|
-
if (
|
|
200
|
-
|
|
201
|
-
|
|
203
|
+
if (pressed && state.startedAt === null)
|
|
204
|
+
state.startedAt = now
|
|
205
|
+
|
|
206
|
+
if (released && state.startedAt !== null) {
|
|
207
|
+
state.count++
|
|
208
|
+
|
|
209
|
+
if (state.count === taps) {
|
|
210
|
+
result = 1
|
|
211
|
+
state.count = 0
|
|
212
|
+
state.startedAt = null
|
|
213
|
+
}
|
|
202
214
|
}
|
|
203
215
|
|
|
204
|
-
|
|
216
|
+
state.wasDown = down
|
|
217
|
+
return result
|
|
205
218
|
}
|
|
206
219
|
|
|
207
220
|
case "mod": {
|
package/s/lib/input/test.ts
CHANGED
|
@@ -103,16 +103,28 @@ export default suite({
|
|
|
103
103
|
expect(inputs.sum).is(0)
|
|
104
104
|
}),
|
|
105
105
|
|
|
106
|
-
"
|
|
107
|
-
const {sample, solve} = quickbind(
|
|
108
|
-
|
|
109
|
-
)
|
|
110
|
-
|
|
111
|
-
for (const value of [1, 0, 1, 0])
|
|
106
|
+
"single tap": test(async() => {
|
|
107
|
+
const {sample, solve} = quickbind(["taps", 1, 400, "KeyE"])
|
|
108
|
+
const hit = (time: number, value: number) => {
|
|
112
109
|
sample({code: "KeyE", value})
|
|
110
|
+
return solve(time).value
|
|
111
|
+
}
|
|
112
|
+
expect(hit(0, 0)).is(0)
|
|
113
|
+
expect(hit(100, 1)).is(0)
|
|
114
|
+
expect(hit(200, 0)).is(1)
|
|
115
|
+
}),
|
|
113
116
|
|
|
114
|
-
|
|
115
|
-
|
|
117
|
+
"double tap": test(async() => {
|
|
118
|
+
const {sample, solve} = quickbind(["taps", 2, 400, "KeyE"])
|
|
119
|
+
const hit = (time: number, value: number) => {
|
|
120
|
+
sample({code: "KeyE", value})
|
|
121
|
+
return solve(time).value
|
|
122
|
+
}
|
|
123
|
+
expect(hit(0, 0)).is(0)
|
|
124
|
+
expect(hit(100, 1)).is(0)
|
|
125
|
+
expect(hit(200, 0)).is(0)
|
|
126
|
+
expect(hit(300, 1)).is(0)
|
|
127
|
+
expect(hit(400, 0)).is(1)
|
|
116
128
|
}),
|
|
117
129
|
|
|
118
130
|
"held across empty frames": test(async() => {
|
|
@@ -136,30 +148,29 @@ export default suite({
|
|
|
136
148
|
expect(solve(900).value).is(1)
|
|
137
149
|
}),
|
|
138
150
|
|
|
139
|
-
"taps
|
|
151
|
+
"taps vs held": test(async() => {
|
|
140
152
|
const {sample, solve} = quickbind({
|
|
141
|
-
|
|
142
|
-
|
|
153
|
+
tappy: ["taps", 1, 300, "KeyE"],
|
|
154
|
+
holdy: ["held", 300, "KeyE"],
|
|
143
155
|
})
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
expect(
|
|
153
|
-
expect(
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
expect(
|
|
158
|
-
expect(
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
expect(
|
|
162
|
-
expect(holding.holder.value).is(1)
|
|
156
|
+
const hit = (time: number, value: number) => {
|
|
157
|
+
sample({code: "KeyE", value})
|
|
158
|
+
return solve(time)
|
|
159
|
+
}
|
|
160
|
+
const __up = (time: number) => hit(time, 0)
|
|
161
|
+
const DOWN = (time: number) => hit(time, 1)
|
|
162
|
+
|
|
163
|
+
expect(__up(1000).tappy.value).is(0)
|
|
164
|
+
expect(DOWN(1100).tappy.value).is(0)
|
|
165
|
+
expect(__up(1200).tappy.value).is(1)
|
|
166
|
+
expect(__up(1300).tappy.value).is(0)
|
|
167
|
+
|
|
168
|
+
expect(__up(2000).holdy.value).is(0)
|
|
169
|
+
expect(DOWN(2100).holdy.value).is(0)
|
|
170
|
+
expect(DOWN(2200).holdy.value).is(0)
|
|
171
|
+
expect(DOWN(2300).holdy.value).is(0)
|
|
172
|
+
expect(DOWN(2400).holdy.value).is(1)
|
|
173
|
+
expect(__up(2500).tappy.value).is(0) // FAILS
|
|
163
174
|
}),
|
|
164
175
|
|
|
165
176
|
"independent controller consumers": test(async() => {
|
|
@@ -1,31 +1,44 @@
|
|
|
1
1
|
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import {disposer, ev} from "@e280/stz"
|
|
4
3
|
import {Hub} from "../hub.js"
|
|
5
4
|
import {asDeck} from "../types.js"
|
|
5
|
+
import {setupKv} from "./utils/kv.js"
|
|
6
6
|
import {SetupParams} from "./types.js"
|
|
7
7
|
import {Controls} from "../controls/controls.js"
|
|
8
8
|
import {Bindings} from "../../../input/types.js"
|
|
9
9
|
import {firstPreset} from "../utils/first-preset.js"
|
|
10
|
+
import {setupBroadcastChannel} from "./utils/broadcast-channel.js"
|
|
10
11
|
|
|
11
12
|
export async function setupDeck<B extends Bindings>(params: SetupParams<B>) {
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
13
|
+
const {dispose, schedule, disposable} = disposer()
|
|
14
|
+
|
|
15
|
+
const namespace = params.namespace
|
|
16
|
+
? `tact_${params.namespace}`
|
|
17
|
+
: "tact"
|
|
18
|
+
|
|
19
|
+
const {kv} = disposable(
|
|
20
|
+
await setupKv(namespace, params.kv)
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
const {broadcastChannel} = disposable(
|
|
24
|
+
await setupBroadcastChannel(namespace, params.broadcastChannel)
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const {maxPlayers} = params
|
|
29
|
+
const {bindings} = firstPreset(params.presets).profile
|
|
30
|
+
const hub = new Hub(bindings)
|
|
31
|
+
const controls = new Controls({...params, kv})
|
|
32
|
+
|
|
33
|
+
schedule(ev(broadcastChannel, {message: () => controls.load()}))
|
|
34
|
+
schedule(controls.on(() => broadcastChannel.postMessage({})))
|
|
35
|
+
|
|
36
|
+
await controls.load()
|
|
37
|
+
return asDeck({hub, controls, maxPlayers, dispose})
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
dispose()
|
|
41
|
+
throw error
|
|
42
|
+
}
|
|
30
43
|
}
|
|
31
44
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
|
|
2
|
+
import {disposer} from "@e280/stz"
|
|
2
3
|
import {register} from "@e280/sly"
|
|
3
4
|
|
|
4
5
|
import {setupDeck} from "./deck.js"
|
|
5
6
|
import {SetupParams} from "./types.js"
|
|
6
|
-
import {Collector} from "../collector.js"
|
|
7
7
|
import {Bindings} from "../../../input/types.js"
|
|
8
8
|
import {autoGamepads} from "../auto-gamepads.js"
|
|
9
9
|
import {ellipsis} from "../../../utils/ellipsis.js"
|
|
@@ -23,9 +23,10 @@ import devicesSvg from "../../icons/akar/devices.svg.js"
|
|
|
23
23
|
import gameControllerSvg from "../../icons/akar/game-controller.svg.js"
|
|
24
24
|
|
|
25
25
|
export async function setupTact<B extends Bindings>(params: SetupParams<B>) {
|
|
26
|
-
const
|
|
26
|
+
const {dispose, schedule, disposable} = disposer()
|
|
27
|
+
|
|
28
|
+
const deck = disposable(await setupDeck(params))
|
|
27
29
|
const first = firstPreset(deck.controls.presets)
|
|
28
|
-
const collector = new Collector(first.profile.bindings)
|
|
29
30
|
const visualizer = new Visualizer()
|
|
30
31
|
|
|
31
32
|
const elements = {
|
|
@@ -36,7 +37,7 @@ export async function setupTact<B extends Bindings>(params: SetupParams<B>) {
|
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
function gamepads(defaultProfileKey?: string) {
|
|
39
|
-
|
|
40
|
+
const stop = autoGamepads(deck.hub, (controller, source) => {
|
|
40
41
|
const handle = `${source.pad.gamepad.id} ${source.pad.gamepad.index + 1}`
|
|
41
42
|
deck.controls.attach(controller, {
|
|
42
43
|
icon: gameControllerSvg,
|
|
@@ -47,18 +48,20 @@ export async function setupTact<B extends Bindings>(params: SetupParams<B>) {
|
|
|
47
48
|
})
|
|
48
49
|
return () => deck.controls.detach(controller)
|
|
49
50
|
})
|
|
51
|
+
schedule(stop)
|
|
52
|
+
return stop
|
|
50
53
|
}
|
|
51
54
|
|
|
52
55
|
function mouseKeyboard() {
|
|
53
56
|
const {key, profile} = first
|
|
54
57
|
|
|
55
|
-
const controller = new Controller({
|
|
58
|
+
const controller = disposable(new Controller({
|
|
56
59
|
bindings: profile.bindings,
|
|
57
|
-
source: new Sources(
|
|
58
|
-
new KeyboardSource(),
|
|
59
|
-
new PointerSource(),
|
|
60
|
-
),
|
|
61
|
-
})
|
|
60
|
+
source: disposable(new Sources(
|
|
61
|
+
disposable(new KeyboardSource()),
|
|
62
|
+
disposable(new PointerSource()),
|
|
63
|
+
)),
|
|
64
|
+
}))
|
|
62
65
|
|
|
63
66
|
deck.hub.plug(controller)
|
|
64
67
|
|
|
@@ -70,6 +73,11 @@ export async function setupTact<B extends Bindings>(params: SetupParams<B>) {
|
|
|
70
73
|
defaultProfileKey: key,
|
|
71
74
|
})
|
|
72
75
|
|
|
76
|
+
schedule(() => {
|
|
77
|
+
deck.hub.remove(controller)
|
|
78
|
+
deck.controls.detach(controller)
|
|
79
|
+
})
|
|
80
|
+
|
|
73
81
|
return controller
|
|
74
82
|
}
|
|
75
83
|
|
|
@@ -79,12 +87,13 @@ export async function setupTact<B extends Bindings>(params: SetupParams<B>) {
|
|
|
79
87
|
|
|
80
88
|
return {
|
|
81
89
|
deck,
|
|
82
|
-
collector,
|
|
83
90
|
gamepads,
|
|
84
91
|
mouseKeyboard,
|
|
85
92
|
visualizer,
|
|
86
93
|
elements,
|
|
87
94
|
registerElements,
|
|
95
|
+
encodeAll: deck.hub.encodeAll.bind(deck.hub),
|
|
96
|
+
dispose,
|
|
88
97
|
}
|
|
89
98
|
}
|
|
90
99
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
|
|
2
|
+
export async function setupBroadcastChannel(
|
|
3
|
+
namespace: string,
|
|
4
|
+
broadcastChannel: BroadcastChannel | undefined,
|
|
5
|
+
) {
|
|
6
|
+
|
|
7
|
+
let dispose = () => {}
|
|
8
|
+
|
|
9
|
+
if (!broadcastChannel) {
|
|
10
|
+
const bc = new BroadcastChannel(namespace)
|
|
11
|
+
broadcastChannel = bc
|
|
12
|
+
dispose = () => bc.close()
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return {broadcastChannel, dispose}
|
|
16
|
+
}
|
|
17
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
|
|
2
|
+
import {IdbMagazine, idbOpen, Kv} from "@e280/kv"
|
|
3
|
+
|
|
4
|
+
export async function setupKv(
|
|
5
|
+
namespace: string,
|
|
6
|
+
kv: Kv | undefined,
|
|
7
|
+
) {
|
|
8
|
+
|
|
9
|
+
let dispose = () => {}
|
|
10
|
+
|
|
11
|
+
if (!kv) {
|
|
12
|
+
const idb = await idbOpen(namespace, "kv")
|
|
13
|
+
const magazine = new IdbMagazine(idb)
|
|
14
|
+
kv = new Kv(magazine)
|
|
15
|
+
dispose = () => idb.db.close()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return {kv, dispose}
|
|
19
|
+
}
|
|
20
|
+
|
package/s/lib/ui/deck/types.ts
CHANGED
|
@@ -4,7 +4,6 @@ import {Bindings} from "../../input/types.js"
|
|
|
4
4
|
import {Controls} from "./controls/controls.js"
|
|
5
5
|
|
|
6
6
|
export type PlayerId = number
|
|
7
|
-
|
|
8
7
|
export type ProfileKey = string
|
|
9
8
|
|
|
10
9
|
export type Profile<B extends Bindings = any> = {
|
|
@@ -19,6 +18,7 @@ export type Deck<B extends Bindings = any> = {
|
|
|
19
18
|
hub: Hub<B>
|
|
20
19
|
maxPlayers: number
|
|
21
20
|
controls: Controls<B>
|
|
21
|
+
dispose: () => void
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
export const asDeck = <B extends Bindings>(deck: Deck<B>) => deck
|
|
@@ -27,17 +27,17 @@ export const [CustomizeView, makeCustomizeElement] = shadowDuplex((options: {con
|
|
|
27
27
|
? profiles.get(profile.basedOn)
|
|
28
28
|
: profile
|
|
29
29
|
|
|
30
|
-
const updateBindings = (bindings: Bindings) => {
|
|
30
|
+
const updateBindings = async(bindings: Bindings) => {
|
|
31
31
|
let key = $profileKey()
|
|
32
32
|
|
|
33
33
|
if (profiles.isPreset(key)) {
|
|
34
34
|
const basedOn = key
|
|
35
35
|
const freshKey = makeId()
|
|
36
36
|
key = freshKey
|
|
37
|
-
profiles.clone(freshKey, basedOn)
|
|
37
|
+
await profiles.clone(freshKey, basedOn)
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
profiles.update(key, {bindings})
|
|
40
|
+
await profiles.update(key, {bindings})
|
|
41
41
|
$profileKey(key)
|
|
42
42
|
}
|
|
43
43
|
|