@benev/tact 0.4.0-5 → 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.
Files changed (44) hide show
  1. package/README.md +100 -41
  2. package/package.json +1 -1
  3. package/s/index.html.ts +1 -1
  4. package/s/lib/input/evaluate/context.ts +5 -1
  5. package/s/lib/input/evaluate/evaluate.ts +27 -14
  6. package/s/lib/input/test.ts +41 -30
  7. package/s/lib/ui/deck/auto-gamepads.ts +1 -0
  8. package/s/lib/ui/deck/setup/deck.ts +33 -20
  9. package/s/lib/ui/deck/setup/tact.ts +23 -10
  10. package/s/lib/ui/deck/setup/types.ts +1 -0
  11. package/s/lib/ui/deck/setup/utils/broadcast-channel.ts +17 -0
  12. package/s/lib/ui/deck/setup/utils/kv.ts +20 -0
  13. package/s/lib/ui/deck/types.ts +1 -1
  14. package/s/lib/ui/views/customize/view.ts +3 -3
  15. package/x/demo/main.bundle.min.js +58 -58
  16. package/x/demo/main.bundle.min.js.map +4 -4
  17. package/x/gamepad/index.html +1 -1
  18. package/x/index.html +3 -3
  19. package/x/index.html.js +1 -1
  20. package/x/index.html.js.map +1 -1
  21. package/x/lib/input/evaluate/context.d.ts +3 -1
  22. package/x/lib/input/evaluate/context.js.map +1 -1
  23. package/x/lib/input/evaluate/evaluate.js +24 -11
  24. package/x/lib/input/evaluate/evaluate.js.map +1 -1
  25. package/x/lib/input/test.d.ts +3 -2
  26. package/x/lib/input/test.js +39 -22
  27. package/x/lib/input/test.js.map +1 -1
  28. package/x/lib/ui/deck/auto-gamepads.js +1 -0
  29. package/x/lib/ui/deck/auto-gamepads.js.map +1 -1
  30. package/x/lib/ui/deck/setup/deck.js +23 -13
  31. package/x/lib/ui/deck/setup/deck.js.map +1 -1
  32. package/x/lib/ui/deck/setup/tact.d.ts +2 -0
  33. package/x/lib/ui/deck/setup/tact.js +18 -7
  34. package/x/lib/ui/deck/setup/tact.js.map +1 -1
  35. package/x/lib/ui/deck/setup/types.d.ts +1 -0
  36. package/x/lib/ui/deck/setup/utils/broadcast-channel.d.ts +4 -0
  37. package/x/lib/ui/deck/setup/utils/broadcast-channel.js +10 -0
  38. package/x/lib/ui/deck/setup/utils/broadcast-channel.js.map +1 -0
  39. package/x/lib/ui/deck/setup/utils/kv.d.ts +5 -0
  40. package/x/lib/ui/deck/setup/utils/kv.js +12 -0
  41. package/x/lib/ui/deck/setup/utils/kv.js.map +1 -0
  42. package/x/lib/ui/deck/types.d.ts +1 -0
  43. package/x/lib/ui/views/customize/view.js +3 -3
  44. 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
- **tact is a user-input toolkit for web games.**
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
- 🧩 expressive bindings json dsl.
12
- 🎨 bindings are user-customizable.
13
- 🫂 multiplayer and splitscreen capable.
14
- 🧬 binary for efficient workers and networking.
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
  ![](https://i.imgur.com/dCAlD14.png)
@@ -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
- - 💡 *bindings is a tiny json dsl. see [expression.ts](./s/lib/input/expression.ts).*
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
- - 🍏 **setup tact.**
60
+ - ⚙️ **setup tact.**
59
61
  ```ts
60
62
  import {setupTact} from "@benev/tact"
61
63
 
@@ -64,41 +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
- - 💡 *set `maxPlayers` to 1 for single player.*
68
- - 🍏 **put the `<tact-deck>` in your html page.**
69
- ```html
70
- <tact-deck></tact-deck>
71
- ```
72
- - 💡 *you also have each panel, `<tact-controllers>` and `<tact-customize>`.*
73
- - 💡 *you can customize the colors with css.*
74
- ```html
75
- <style>
76
- html {
77
- --tact-prime: aqua;
78
- --tact-bg: black;
79
- }
80
- </style>
81
- ```
82
- - 🎉 **now users can configure controllers and customize bindings!**
83
-
84
-
85
-
86
- <br/>
87
-
88
- ## 🎮 encoding, decoding, and your game logic.
89
-
90
- - 🍏 **now it's time for the real action!**
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.*
73
+ - 🍏 **encoding, decoding, and your game logic.**
91
74
  ```ts
92
75
  import {Collector} from "@benev/tact"
93
76
 
94
77
  const collector = new Collector(bindings)
95
78
 
96
79
  setInterval(() => {
97
- const buffer = tact.deck.hub.encodeAll()
80
+ const buffer = tact.encodeAll()
98
81
  for (const {playerId, inputs} of collector.decodeAll(buffer)) {
99
82
 
100
83
  // your game logic can read these handy input values.
101
- inputs.jump.value // values are numbers 0 to 1.
84
+ inputs.jump.value // button-like values are numbers 0 to 1.
102
85
  inputs.jump.was // previous value (to detect up/down).
103
86
  inputs.jump.sum // sum of values reported since last frame.
104
87
 
@@ -107,16 +90,31 @@ i made tact because i want to make indie web game development easier. user input
107
90
  }
108
91
  }, 16.67)
109
92
  ```
110
- - 💡 *you could stream the buffers across the network to a remote collector.*
111
- - 💡 *maybe imagine having the tact deck on the clientside, and the collector on the serverside (or a web worker!).*
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!).*
95
+ - 🍏 **put the `<tact-deck>` element in your html page.**
96
+ ```html
97
+ <tact-deck></tact-deck>
98
+ ```
99
+ - 💡 *or just use the sub-panels directly, `<tact-controllers>` and `<tact-customize>`.*
100
+ - 💡 *you can customize the colors with css.*
101
+ ```html
102
+ <style>
103
+ html {
104
+ --tact-prime: aqua;
105
+ }
106
+ </style>
107
+ ```
108
+ - 🎉 **you're done! your users can manage controllers and customize bindings!**
112
109
 
113
110
 
114
111
 
115
112
  <br/>
116
113
 
117
114
  ## 🎮 get down to brass tacks.
115
+ > *for simpler apps, you can use tact's composable parts more directly.*
118
116
 
119
- - 🍏 **pov: your app isn't a game, no multiplayer, no gamepads.**
117
+ - ⚙️ **pov: your app isn't a game, no multiplayer, no gamepads.**
120
118
  ```ts
121
119
  import {setupTact, InputDecoder} from "@benev/tact"
122
120
 
@@ -134,7 +132,7 @@ i made tact because i want to make indie web game development easier. user input
134
132
  }, 16.67)
135
133
  ```
136
134
  - 💡 *then the only ui you need is the `<tact-customize>` panel (also available as a sly view `CustomizeView`).*
137
- - 🍏 **pov: your app doesn't even want any ui, or persistence.**
135
+ - ⚙️ **pov: your app doesn't even want any ui, or persistence.**
138
136
  ```ts
139
137
  import {Controller, InputDecoder, KeyboardSource, PointerSource, Sources} from "@benev/tact"
140
138
 
@@ -153,6 +151,67 @@ i made tact because i want to make indie web game development easier. user input
153
151
 
154
152
 
155
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
+
156
215
  <br/><br/>
157
216
 
158
217
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@benev/tact",
3
- "version": "0.4.0-5",
3
+ "version": "0.4.0-7",
4
4
  "description": "🎮 from keypress to couch co-op",
5
5
  "license": "MIT",
6
6
  "author": "Chase Moskal <chasemoskal@gmail.com>",
package/s/index.html.ts CHANGED
@@ -4,7 +4,7 @@ import {template, html, dataSvgEmoji, socialCard} from "@e280/scute"
4
4
  const title = "@benev/tact"
5
5
  const domain = "tact.benev.gg"
6
6
  const favicon = "/assets/b.png"
7
- const description = "🎮 keybindings and couch co-op"
7
+ const description = "🎮 from keypress to couch co-op"
8
8
 
9
9
  export default template(import.meta.url, async orb => html`
10
10
  <!doctype html>
@@ -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, {events: number[]}>()
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 {arraylimit, guarantee, isHappy, isNumber, isString} from "@e280/stz"
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 presses = e(subexpression)
184
+ const down = isDown(e(subexpression))
185
185
  const now = context.clock.time
186
186
 
187
- const tapped = guarantee(context.tapping, expression, () => ({
188
- events: [] as number[],
187
+ const state = guarantee(context.tapping, expression, () => ({
188
+ wasDown: false,
189
+ startedAt: null as number | null,
190
+ count: 0,
189
191
  }))
190
192
 
191
- for (let i = 0; i < presses; i++)
192
- tapped.events.push(now)
193
+ const pressed = down && !state.wasDown
194
+ const released = !down && state.wasDown
193
195
 
194
- tapped.events = arraylimit(
195
- tapped.events.filter(time => time >= now - ms),
196
- 128,
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 (tapped.events.length >= taps) {
200
- tapped.events = []
201
- return 1
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
- return 0
216
+ state.wasDown = down
217
+ return result
205
218
  }
206
219
 
207
220
  case "mod": {
@@ -103,16 +103,28 @@ export default suite({
103
103
  expect(inputs.sum).is(0)
104
104
  }),
105
105
 
106
- "double tap in one frame": test(async() => {
107
- const {sample, solve} = quickbind(
108
- ["taps", 2, 300, ["down", "KeyE"]]
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
- expect(solve(100).value).is(1)
115
- expect(solve(200).value).is(0)
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 and holds coexist": test(async() => {
151
+ "taps vs held": test(async() => {
140
152
  const {sample, solve} = quickbind({
141
- tapper: ["taps", 1, 10, ["down", "KeyE"]],
142
- holder: ["held", 10, "KeyE"],
153
+ tappy: ["taps", 1, 300, "KeyE"],
154
+ holdy: ["held", 300, "KeyE"],
143
155
  })
144
-
145
- solve(0)
146
-
147
- sample({code: "KeyE", value: 1})
148
- expect(solve(1).tapper.value).is(1)
149
-
150
- sample({code: "KeyE", value: 0})
151
- const released = solve(2)
152
- expect(released.tapper.value).is(0)
153
- expect(released.holder.value).is(0)
154
-
155
- sample({code: "KeyE", value: 1})
156
- const pressed = solve(3)
157
- expect(pressed.tapper.value).is(1)
158
- expect(pressed.holder.value).is(0)
159
-
160
- const holding = solve(13)
161
- expect(holding.tapper.value).is(0)
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() => {
@@ -21,6 +21,7 @@ export function autoGamepads(
21
21
  const dispose = fn?.(controller, source)
22
22
 
23
23
  return () => {
24
+ controller.dispose()
24
25
  hub.remove(controller)
25
26
  dispose?.()
26
27
  }
@@ -1,31 +1,44 @@
1
1
 
2
- import {IdbMagazine, idbOpen, Kv} from "@e280/kv"
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 kv = params.kv
13
- ?? new Kv(new IdbMagazine(await idbOpen("tact", "kv")))
14
-
15
- const broadcastChannel = params.broadcastChannel
16
- ?? new BroadcastChannel("tact")
17
-
18
- const {maxPlayers} = params
19
- const {bindings} = firstPreset(params.presets).profile
20
-
21
- const hub = new Hub(bindings)
22
- const controls = new Controls({...params, kv})
23
-
24
- await controls.load()
25
-
26
- broadcastChannel.onmessage = () => controls.load()
27
- controls.on(() => broadcastChannel.postMessage({}))
28
-
29
- return asDeck({hub, controls, maxPlayers})
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,4 +1,5 @@
1
1
 
2
+ import {disposer} from "@e280/stz"
2
3
  import {register} from "@e280/sly"
3
4
 
4
5
  import {setupDeck} from "./deck.js"
@@ -22,7 +23,10 @@ import devicesSvg from "../../icons/akar/devices.svg.js"
22
23
  import gameControllerSvg from "../../icons/akar/game-controller.svg.js"
23
24
 
24
25
  export async function setupTact<B extends Bindings>(params: SetupParams<B>) {
25
- const deck = await setupDeck(params)
26
+ const {dispose, schedule, disposable} = disposer()
27
+
28
+ const deck = disposable(await setupDeck(params))
29
+ const first = firstPreset(deck.controls.presets)
26
30
  const visualizer = new Visualizer()
27
31
 
28
32
  const elements = {
@@ -33,29 +37,31 @@ export async function setupTact<B extends Bindings>(params: SetupParams<B>) {
33
37
  }
34
38
 
35
39
  function gamepads(defaultProfileKey?: string) {
36
- return autoGamepads(deck.hub, (controller, source) => {
40
+ const stop = autoGamepads(deck.hub, (controller, source) => {
37
41
  const handle = `${source.pad.gamepad.id} ${source.pad.gamepad.index + 1}`
38
42
  deck.controls.attach(controller, {
39
43
  icon: gameControllerSvg,
40
44
  handle,
41
45
  tooltip: handle,
42
46
  name: ellipsis(handle, 32),
43
- defaultProfileKey: defaultProfileKey ?? firstPreset(deck.controls.presets).key,
47
+ defaultProfileKey: defaultProfileKey ?? first.key,
44
48
  })
45
49
  return () => deck.controls.detach(controller)
46
50
  })
51
+ schedule(stop)
52
+ return stop
47
53
  }
48
54
 
49
55
  function mouseKeyboard() {
50
- const {key, profile} = firstPreset(deck.controls.presets)
56
+ const {key, profile} = first
51
57
 
52
- const controller = new Controller({
58
+ const controller = disposable(new Controller({
53
59
  bindings: profile.bindings,
54
- source: new Sources(
55
- new KeyboardSource(),
56
- new PointerSource(),
57
- ),
58
- })
60
+ source: disposable(new Sources(
61
+ disposable(new KeyboardSource()),
62
+ disposable(new PointerSource()),
63
+ )),
64
+ }))
59
65
 
60
66
  deck.hub.plug(controller)
61
67
 
@@ -67,6 +73,11 @@ export async function setupTact<B extends Bindings>(params: SetupParams<B>) {
67
73
  defaultProfileKey: key,
68
74
  })
69
75
 
76
+ schedule(() => {
77
+ deck.hub.remove(controller)
78
+ deck.controls.detach(controller)
79
+ })
80
+
70
81
  return controller
71
82
  }
72
83
 
@@ -81,6 +92,8 @@ export async function setupTact<B extends Bindings>(params: SetupParams<B>) {
81
92
  visualizer,
82
93
  elements,
83
94
  registerElements,
95
+ encodeAll: deck.hub.encodeAll.bind(deck.hub),
96
+ dispose,
84
97
  }
85
98
  }
86
99
 
@@ -6,6 +6,7 @@ import {Bindings} from "../../../input/types.js"
6
6
  export type SetupParams<B extends Bindings> = {
7
7
  maxPlayers: number
8
8
  presets: Presets<B>
9
+ namespace?: string
9
10
  kv?: Kv
10
11
  broadcastChannel?: BroadcastChannel
11
12
  }
@@ -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
+
@@ -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