@benev/tact 0.4.0-1 → 0.4.0-10

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 (122) hide show
  1. package/README.md +134 -46
  2. package/package.json +7 -6
  3. package/s/demo/main.css +38 -3
  4. package/s/index.html.ts +26 -20
  5. package/s/lib/index.ts +1 -1
  6. package/s/lib/input/controller.ts +4 -1
  7. package/s/lib/input/data/intent/decode.ts +2 -2
  8. package/s/lib/input/data/intent/encode.ts +2 -2
  9. package/s/lib/input/data/intent/minihash.ts +5 -0
  10. package/s/lib/input/decoder.ts +2 -2
  11. package/s/lib/input/evaluate/context.ts +5 -1
  12. package/s/lib/input/evaluate/evaluate.ts +27 -14
  13. package/s/lib/input/investigate/hash-bindings.ts +5 -3
  14. package/s/lib/input/investigate/investigate.ts +24 -9
  15. package/s/lib/input/test.ts +52 -30
  16. package/s/lib/input/testing/quickbind.ts +3 -3
  17. package/s/lib/ui/deck/auto-gamepads.ts +1 -0
  18. package/s/lib/ui/deck/controls/internal/data.ts +7 -5
  19. package/s/lib/ui/deck/hub.ts +4 -3
  20. package/s/lib/ui/deck/players.ts +73 -0
  21. package/s/lib/ui/deck/setup/deck.ts +33 -20
  22. package/s/lib/ui/deck/setup/tact.ts +26 -11
  23. package/s/lib/ui/deck/setup/types.ts +1 -0
  24. package/s/lib/ui/deck/setup/utils/broadcast-channel.ts +17 -0
  25. package/s/lib/ui/deck/setup/utils/kv.ts +20 -0
  26. package/s/lib/ui/deck/types.ts +1 -1
  27. package/s/lib/ui/views/controllers/style.css.ts +1 -1
  28. package/s/lib/ui/views/controllers/view.ts +3 -2
  29. package/s/lib/ui/views/customize/style.css.ts +1 -1
  30. package/s/lib/ui/views/customize/view.ts +6 -5
  31. package/s/lib/ui/views/deck/style.css.ts +1 -1
  32. package/s/lib/ui/views/deck/view.ts +13 -7
  33. package/s/lib/ui/views/theme.css.ts +1 -1
  34. package/s/lib/ui/views/visualizer/style.css.ts +8 -6
  35. package/s/lib/ui/views/visualizer/view.ts +1 -0
  36. package/s/lib/utils/hash.ts +47 -0
  37. package/x/demo/main.bundle.min.js +330 -326
  38. package/x/demo/main.bundle.min.js.map +4 -4
  39. package/x/demo/main.css +38 -3
  40. package/x/gamepad/index.html +2 -2
  41. package/x/gamepad/main.bundle.min.js +8 -8
  42. package/x/gamepad/main.bundle.min.js.map +4 -4
  43. package/x/index.html +28 -22
  44. package/x/index.html.js +26 -20
  45. package/x/index.html.js.map +1 -1
  46. package/x/lib/index.d.ts +1 -1
  47. package/x/lib/index.js +1 -1
  48. package/x/lib/index.js.map +1 -1
  49. package/x/lib/input/controller.js +1 -1
  50. package/x/lib/input/controller.js.map +1 -1
  51. package/x/lib/input/data/intent/decode.d.ts +1 -1
  52. package/x/lib/input/data/intent/decode.js +2 -2
  53. package/x/lib/input/data/intent/decode.js.map +1 -1
  54. package/x/lib/input/data/intent/encode.d.ts +1 -1
  55. package/x/lib/input/data/intent/encode.js +2 -2
  56. package/x/lib/input/data/intent/encode.js.map +1 -1
  57. package/x/lib/input/data/intent/minihash.d.ts +1 -0
  58. package/x/lib/input/data/intent/minihash.js +4 -0
  59. package/x/lib/input/data/intent/minihash.js.map +1 -0
  60. package/x/lib/input/decoder.d.ts +1 -1
  61. package/x/lib/input/decoder.js +2 -2
  62. package/x/lib/input/decoder.js.map +1 -1
  63. package/x/lib/input/evaluate/context.d.ts +3 -1
  64. package/x/lib/input/evaluate/context.js.map +1 -1
  65. package/x/lib/input/evaluate/evaluate.js +24 -11
  66. package/x/lib/input/evaluate/evaluate.js.map +1 -1
  67. package/x/lib/input/investigate/hash-bindings.d.ts +1 -1
  68. package/x/lib/input/investigate/hash-bindings.js +5 -3
  69. package/x/lib/input/investigate/hash-bindings.js.map +1 -1
  70. package/x/lib/input/investigate/investigate.d.ts +2 -1
  71. package/x/lib/input/investigate/investigate.js +14 -9
  72. package/x/lib/input/investigate/investigate.js.map +1 -1
  73. package/x/lib/input/test.d.ts +4 -2
  74. package/x/lib/input/test.js +49 -22
  75. package/x/lib/input/test.js.map +1 -1
  76. package/x/lib/input/testing/quickbind.d.ts +3 -2
  77. package/x/lib/input/testing/quickbind.js +3 -3
  78. package/x/lib/input/testing/quickbind.js.map +1 -1
  79. package/x/lib/ui/deck/auto-gamepads.js +1 -0
  80. package/x/lib/ui/deck/auto-gamepads.js.map +1 -1
  81. package/x/lib/ui/deck/controls/internal/data.d.ts +1 -1
  82. package/x/lib/ui/deck/controls/internal/data.js +4 -1
  83. package/x/lib/ui/deck/controls/internal/data.js.map +1 -1
  84. package/x/lib/ui/deck/hub.js +4 -3
  85. package/x/lib/ui/deck/hub.js.map +1 -1
  86. package/x/lib/ui/deck/players.d.ts +16 -0
  87. package/x/lib/ui/deck/players.js +56 -0
  88. package/x/lib/ui/deck/players.js.map +1 -0
  89. package/x/lib/ui/deck/setup/deck.js +23 -13
  90. package/x/lib/ui/deck/setup/deck.js.map +1 -1
  91. package/x/lib/ui/deck/setup/tact.d.ts +3 -0
  92. package/x/lib/ui/deck/setup/tact.js +19 -8
  93. package/x/lib/ui/deck/setup/tact.js.map +1 -1
  94. package/x/lib/ui/deck/setup/types.d.ts +1 -0
  95. package/x/lib/ui/deck/setup/utils/broadcast-channel.d.ts +4 -0
  96. package/x/lib/ui/deck/setup/utils/broadcast-channel.js +10 -0
  97. package/x/lib/ui/deck/setup/utils/broadcast-channel.js.map +1 -0
  98. package/x/lib/ui/deck/setup/utils/kv.d.ts +5 -0
  99. package/x/lib/ui/deck/setup/utils/kv.js +12 -0
  100. package/x/lib/ui/deck/setup/utils/kv.js.map +1 -0
  101. package/x/lib/ui/deck/types.d.ts +1 -0
  102. package/x/lib/ui/views/controllers/style.css.js +1 -1
  103. package/x/lib/ui/views/controllers/view.js +3 -2
  104. package/x/lib/ui/views/controllers/view.js.map +1 -1
  105. package/x/lib/ui/views/customize/style.css.js +1 -1
  106. package/x/lib/ui/views/customize/view.js +6 -5
  107. package/x/lib/ui/views/customize/view.js.map +1 -1
  108. package/x/lib/ui/views/deck/style.css.js +1 -1
  109. package/x/lib/ui/views/deck/view.js +13 -6
  110. package/x/lib/ui/views/deck/view.js.map +1 -1
  111. package/x/lib/ui/views/theme.css.js +1 -1
  112. package/x/lib/ui/views/visualizer/style.css.js +8 -6
  113. package/x/lib/ui/views/visualizer/style.css.js.map +1 -1
  114. package/x/lib/ui/views/visualizer/view.js +1 -0
  115. package/x/lib/ui/views/visualizer/view.js.map +1 -1
  116. package/x/lib/utils/hash.d.ts +1 -0
  117. package/x/lib/utils/hash.js +47 -0
  118. package/x/lib/utils/hash.js.map +1 -0
  119. package/s/lib/ui/deck/collector.ts +0 -52
  120. package/x/lib/ui/deck/collector.d.ts +0 -13
  121. package/x/lib/ui/deck/collector.js +0 -35
  122. package/x/lib/ui/deck/collector.js.map +0 -1
@@ -5,6 +5,7 @@ import {asBindings} from "./types.js"
5
5
  import {Controller} from "./controller.js"
6
6
  import {PlainSource} from "./sources/plain.js"
7
7
  import {quickbind} from "./testing/quickbind.js"
8
+ import {isCompatible} from "./investigate/is-compatible.js"
8
9
 
9
10
  import parcel from "./data/test.js"
10
11
 
@@ -23,6 +24,16 @@ export default suite({
23
24
  expect(inputs.was).is(0)
24
25
  }),
25
26
 
27
+ "reordered bindings are equivalent": test(async() => {
28
+ const able = quickbind({alpha: "KeyA", bravo: "KeyB"})
29
+ const baker = quickbind({bravo: "KeyB", alpha: "KeyA"})
30
+ expect(isCompatible(able.bindings, baker.bindings)).is(true)
31
+ able.sample({code: "KeyA", value: 1})
32
+ const inputs = baker.decoder.decode(able.controller.encode())
33
+ expect(inputs.alpha.value).is(1)
34
+ expect(inputs.bravo.value).is(0)
35
+ }),
36
+
26
37
  "update bindings": test(async() => {
27
38
  const {sample, solve, controller} = quickbind("KeyA")
28
39
 
@@ -103,16 +114,28 @@ export default suite({
103
114
  expect(inputs.sum).is(0)
104
115
  }),
105
116
 
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])
117
+ "single tap": test(async() => {
118
+ const {sample, solve} = quickbind(["taps", 1, 400, "KeyE"])
119
+ const hit = (time: number, value: number) => {
112
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(1)
126
+ }),
113
127
 
114
- expect(solve(100).value).is(1)
115
- expect(solve(200).value).is(0)
128
+ "double tap": test(async() => {
129
+ const {sample, solve} = quickbind(["taps", 2, 400, "KeyE"])
130
+ const hit = (time: number, value: number) => {
131
+ sample({code: "KeyE", value})
132
+ return solve(time).value
133
+ }
134
+ expect(hit(0, 0)).is(0)
135
+ expect(hit(100, 1)).is(0)
136
+ expect(hit(200, 0)).is(0)
137
+ expect(hit(300, 1)).is(0)
138
+ expect(hit(400, 0)).is(1)
116
139
  }),
117
140
 
118
141
  "held across empty frames": test(async() => {
@@ -136,30 +159,29 @@ export default suite({
136
159
  expect(solve(900).value).is(1)
137
160
  }),
138
161
 
139
- "taps and holds coexist": test(async() => {
162
+ "taps vs held": test(async() => {
140
163
  const {sample, solve} = quickbind({
141
- tapper: ["taps", 1, 10, ["down", "KeyE"]],
142
- holder: ["held", 10, "KeyE"],
164
+ tappy: ["taps", 1, 300, "KeyE"],
165
+ holdy: ["held", 300, "KeyE"],
143
166
  })
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)
167
+ const hit = (time: number, value: number) => {
168
+ sample({code: "KeyE", value})
169
+ return solve(time)
170
+ }
171
+ const __up = (time: number) => hit(time, 0)
172
+ const DOWN = (time: number) => hit(time, 1)
173
+
174
+ expect(__up(1000).tappy.value).is(0)
175
+ expect(DOWN(1100).tappy.value).is(0)
176
+ expect(__up(1200).tappy.value).is(1)
177
+ expect(__up(1300).tappy.value).is(0)
178
+
179
+ expect(__up(2000).holdy.value).is(0)
180
+ expect(DOWN(2100).holdy.value).is(0)
181
+ expect(DOWN(2200).holdy.value).is(0)
182
+ expect(DOWN(2300).holdy.value).is(0)
183
+ expect(DOWN(2400).holdy.value).is(1)
184
+ expect(__up(2500).tappy.value).is(0) // FAILS
163
185
  }),
164
186
 
165
187
  "independent controller consumers": test(async() => {
@@ -1,5 +1,5 @@
1
1
 
2
- import {InputDecoder} from "../decoder.js"
2
+ import {Decoder} from "../decoder.js"
3
3
  import {Controller} from "../controller.js"
4
4
  import {Bindings, Sample} from "../types.js"
5
5
  import {PlainSource} from "../sources/plain.js"
@@ -7,7 +7,7 @@ import {PlainSource} from "../sources/plain.js"
7
7
  export function quickbind<B extends Bindings>(b: B) {
8
8
  const source = new PlainSource()
9
9
  const controller = new Controller({source, bindings: b})
10
- const decoder = new InputDecoder(controller.bindings)
10
+ const decoder = new Decoder(controller.bindings)
11
11
  const {inputs} = decoder
12
12
 
13
13
  const sample = (...samples: Sample[]) =>
@@ -16,6 +16,6 @@ export function quickbind<B extends Bindings>(b: B) {
16
16
  const solve = (time?: number) =>
17
17
  decoder.decode(controller.encode(time))
18
18
 
19
- return {source, controller, decoder, sample, solve, inputs}
19
+ return {bindings: b, source, controller, decoder, sample, solve, inputs}
20
20
  }
21
21
 
@@ -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
  }
@@ -5,21 +5,23 @@ import {got, IdCounter, setEntries, subby} from "@e280/stz"
5
5
  import {Profile, ProfileKey} from "../../types.js"
6
6
  import {Bindings} from "../../../../input/types.js"
7
7
  import {Controller} from "../../../../input/controller.js"
8
+ import {hashBindings} from "../../../../input/investigate/hash-bindings.js"
8
9
  import {ControllerHandle, ControllerMeta, ControlsOptions} from "../types.js"
9
10
 
10
11
  export type ControlsData<B extends Bindings> = (
11
12
  ReturnType<typeof makeControlsData<B>>
12
13
  )
13
14
 
14
- export function makeControlsData<B extends Bindings>({
15
- kv,
16
- presets,
17
- }: ControlsOptions<B>) {
18
-
15
+ export function makeControlsData<B extends Bindings>(options: ControlsOptions<B>) {
16
+ const {presets} = options
19
17
  const fallbackKey = got(Object.keys(presets)[0], "missing presets")
20
18
  const fallbackProfile = got(presets[fallbackKey])
21
19
  const bindings = fallbackProfile.bindings
22
20
 
21
+ const kv = options.kv.scope(
22
+ hashBindings(bindings).toString()
23
+ )
24
+
23
25
  const profiles = {
24
26
  presets: new RMap<ProfileKey, Profile<B>>(),
25
27
  customs: new RMap<ProfileKey, Profile<B>>(),
@@ -5,18 +5,19 @@ import {got, IdCounter} from "@e280/stz"
5
5
  import {PlayerId} from "./types.js"
6
6
  import {Bindings} from "../../input/types.js"
7
7
  import {Controller} from "../../input/controller.js"
8
+ import {minihash} from "../../input/data/intent/minihash.js"
8
9
  import {encodeParcels} from "../../input/data/parcel/encode.js"
9
10
  import {encodeIntents} from "../../input/data/intent/encode.js"
10
11
  import {hashBindings} from "../../input/investigate/hash-bindings.js"
11
12
 
12
13
  export class Hub<B extends Bindings = any> {
13
- #hash
14
+ #minihash
14
15
  #ids = new IdCounter()
15
16
  #unassigned = new RSet<Controller<B>>()
16
17
  #players = new RMap<PlayerId, {controllers: RSet<Controller<B>>}>()
17
18
 
18
19
  constructor(readonly bindings: B) {
19
- this.#hash = hashBindings(bindings)
20
+ this.#minihash = minihash(hashBindings(bindings))
20
21
  }
21
22
 
22
23
  get playerIds() {
@@ -86,7 +87,7 @@ export class Hub<B extends Bindings = any> {
86
87
  .filter(([, value]) => value !== 0)
87
88
  .map(([id, value]) => ({id, value}))
88
89
 
89
- return encodeIntents(this.#hash, changedIntents)
90
+ return encodeIntents(this.#minihash, changedIntents)
90
91
  }
91
92
 
92
93
  encodeAll(time = performance.now()) {
@@ -0,0 +1,73 @@
1
+
2
+ import {RMap} from "@e280/strata"
3
+ import {guarantee} from "@e280/stz"
4
+
5
+ import {PlayerId} from "./types.js"
6
+ import {Bindings} from "../../input/types.js"
7
+ import {Decoder} from "../../input/decoder.js"
8
+ import {decodeParcels} from "../../input/data/parcel/decode.js"
9
+
10
+ export class Players<B extends Bindings> {
11
+ #bindings: B
12
+ #decoders = new RMap<PlayerId, Decoder<B>>()
13
+
14
+ constructor(bindings: B) {
15
+ this.#bindings = bindings
16
+ }
17
+
18
+ decodeAll(...buffers: ArrayBuffer[]) {
19
+ const added: PlayerId[] = []
20
+ const removed: PlayerId[] = []
21
+ const grouped = new Map<PlayerId, ArrayBuffer[]>()
22
+
23
+ for (const buffer of buffers)
24
+ for (const {playerId, intents} of decodeParcels(buffer))
25
+ guarantee(grouped, playerId, () => []).push(intents)
26
+
27
+ for (const [playerId, intents] of grouped) {
28
+ let decoder = this.#decoders.get(playerId)
29
+ if (!decoder) {
30
+ decoder = new Decoder(this.#bindings)
31
+ this.#decoders.set(playerId, decoder)
32
+ added.push(playerId)
33
+ }
34
+ decoder.decode(...intents)
35
+ }
36
+
37
+ for (const playerId of this.#decoders.keys()) {
38
+ if (!grouped.has(playerId)) {
39
+ this.#decoders.delete(playerId)
40
+ removed.push(playerId)
41
+ }
42
+ }
43
+
44
+ return {added, removed}
45
+ }
46
+
47
+ has(playerId: PlayerId) {
48
+ return this.#decoders.has(playerId)
49
+ }
50
+
51
+ get(playerId: PlayerId) {
52
+ return this.#decoders.get(playerId)?.inputs
53
+ }
54
+
55
+ keys() {
56
+ return this.#decoders.keys()
57
+ }
58
+
59
+ *values() {
60
+ for (const {inputs} of this.#decoders.values())
61
+ yield inputs
62
+ }
63
+
64
+ *entries() {
65
+ for (const [playerId, {inputs}] of this.#decoders)
66
+ yield [playerId, inputs] as const
67
+ }
68
+
69
+ [Symbol.iterator]() {
70
+ return this.entries()
71
+ }
72
+ }
73
+
@@ -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,10 +1,12 @@
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
7
  import {Bindings} from "../../../input/types.js"
7
8
  import {autoGamepads} from "../auto-gamepads.js"
9
+ import {ellipsis} from "../../../utils/ellipsis.js"
8
10
  import {firstPreset} from "../utils/first-preset.js"
9
11
  import {Controller} from "../../../input/controller.js"
10
12
  import {Sources} from "../../../input/sources/sources.js"
@@ -19,10 +21,14 @@ import {makeControllersElement} from "../../views/controllers/view.js"
19
21
 
20
22
  import devicesSvg from "../../icons/akar/devices.svg.js"
21
23
  import gameControllerSvg from "../../icons/akar/game-controller.svg.js"
22
- import { ellipsis } from "../../../utils/ellipsis.js"
24
+
25
+ export type Tact = Awaited<ReturnType<typeof setupTact>>
23
26
 
24
27
  export async function setupTact<B extends Bindings>(params: SetupParams<B>) {
25
- const deck = await setupDeck(params)
28
+ const {dispose, schedule, disposable} = disposer()
29
+
30
+ const deck = disposable(await setupDeck(params))
31
+ const first = firstPreset(deck.controls.presets)
26
32
  const visualizer = new Visualizer()
27
33
 
28
34
  const elements = {
@@ -33,29 +39,31 @@ export async function setupTact<B extends Bindings>(params: SetupParams<B>) {
33
39
  }
34
40
 
35
41
  function gamepads(defaultProfileKey?: string) {
36
- return autoGamepads(deck.hub, (controller, source) => {
42
+ const stop = autoGamepads(deck.hub, (controller, source) => {
37
43
  const handle = `${source.pad.gamepad.id} ${source.pad.gamepad.index + 1}`
38
44
  deck.controls.attach(controller, {
39
45
  icon: gameControllerSvg,
40
46
  handle,
41
47
  tooltip: handle,
42
48
  name: ellipsis(handle, 32),
43
- defaultProfileKey: defaultProfileKey ?? firstPreset(deck.controls.presets).key,
49
+ defaultProfileKey: defaultProfileKey ?? first.key,
44
50
  })
45
51
  return () => deck.controls.detach(controller)
46
52
  })
53
+ schedule(stop)
54
+ return stop
47
55
  }
48
56
 
49
57
  function mouseKeyboard() {
50
- const {key, profile} = firstPreset(deck.controls.presets)
58
+ const {key, profile} = first
51
59
 
52
- const controller = new Controller({
60
+ const controller = disposable(new Controller({
53
61
  bindings: profile.bindings,
54
- source: new Sources(
55
- new KeyboardSource(),
56
- new PointerSource(),
57
- ),
58
- })
62
+ source: disposable(new Sources(
63
+ disposable(new KeyboardSource()),
64
+ disposable(new PointerSource()),
65
+ )),
66
+ }))
59
67
 
60
68
  deck.hub.plug(controller)
61
69
 
@@ -67,6 +75,11 @@ export async function setupTact<B extends Bindings>(params: SetupParams<B>) {
67
75
  defaultProfileKey: key,
68
76
  })
69
77
 
78
+ schedule(() => {
79
+ deck.hub.remove(controller)
80
+ deck.controls.detach(controller)
81
+ })
82
+
70
83
  return controller
71
84
  }
72
85
 
@@ -81,6 +94,8 @@ export async function setupTact<B extends Bindings>(params: SetupParams<B>) {
81
94
  visualizer,
82
95
  elements,
83
96
  registerElements,
97
+ encodeAll: deck.hub.encodeAll.bind(deck.hub),
98
+ dispose,
84
99
  }
85
100
  }
86
101
 
@@ -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
@@ -2,7 +2,7 @@
2
2
  import {css} from "lit"
3
3
  export default css`@layer view {
4
4
 
5
- .plate {
5
+ [part="plate"] {
6
6
  user-select: none;
7
7
  display: flex;
8
8
  flex-direction: column;
@@ -1,7 +1,7 @@
1
1
 
2
2
  import {html} from "lit"
3
3
  import {isHappy, when} from "@e280/stz"
4
- import {loot, shadowDuplex, useCss, useOnce} from "@e280/sly"
4
+ import {loot, shadowDuplex, useCss, useName, useOnce} from "@e280/sly"
5
5
 
6
6
  import styleCss from "./style.css.js"
7
7
  import themeCss from "../theme.css.js"
@@ -14,6 +14,7 @@ import {ControllerView} from "./subviews/controller.js"
14
14
  import {Context, droppyNewPlayer} from "../deck/types.js"
15
15
 
16
16
  export const [ControllersView, makeControllersElement] = shadowDuplex((deck: Deck) => {
17
+ useName("tact-controllers")
17
18
  useCss(themeCss, styleCss)
18
19
 
19
20
  const {hub} = deck
@@ -36,7 +37,7 @@ export const [ControllersView, makeControllersElement] = shadowDuplex((deck: Dec
36
37
  }))
37
38
 
38
39
  return html`
39
- <div class=plate ?data-drag-and-drop="${!!context.dnd.dragging}">
40
+ <div part=plate ?data-drag-and-drop="${!!context.dnd.dragging}">
40
41
  <div class=playerlist>
41
42
  ${hub.playerIds.map(renderPlayer)}
42
43
 
@@ -2,7 +2,7 @@
2
2
  import {css} from "lit"
3
3
  export default css`@layer view {
4
4
 
5
- .plate {
5
+ [part="plate"] {
6
6
  display: flex;
7
7
  flex-direction: column;
8
8
  }
@@ -1,7 +1,7 @@
1
1
 
2
2
  import {html} from "lit"
3
3
  import {given, when} from "@e280/stz"
4
- import {shadowDuplex, useCss, useSignal} from "@e280/sly"
4
+ import {shadowDuplex, useCss, useName, useSignal} from "@e280/sly"
5
5
 
6
6
  import styleCss from "./style.css.js"
7
7
  import themeCss from "../theme.css.js"
@@ -16,6 +16,7 @@ import {renderBindings} from "./parts/render-bindings.js"
16
16
  import {ProfileSelectView} from "../profile-select/view.js"
17
17
 
18
18
  export const [CustomizeView, makeCustomizeElement] = shadowDuplex((options: {controls: Controls}) => {
19
+ useName("tact-customize")
19
20
  useCss(themeCss, styleCss)
20
21
  const {controls, controls: {profiles}} = options
21
22
  const $profileKey = useSignal(controls.fallbackProfileKey)
@@ -27,17 +28,17 @@ export const [CustomizeView, makeCustomizeElement] = shadowDuplex((options: {con
27
28
  ? profiles.get(profile.basedOn)
28
29
  : profile
29
30
 
30
- const updateBindings = (bindings: Bindings) => {
31
+ const updateBindings = async(bindings: Bindings) => {
31
32
  let key = $profileKey()
32
33
 
33
34
  if (profiles.isPreset(key)) {
34
35
  const basedOn = key
35
36
  const freshKey = makeId()
36
37
  key = freshKey
37
- profiles.clone(freshKey, basedOn)
38
+ await profiles.clone(freshKey, basedOn)
38
39
  }
39
40
 
40
- profiles.update(key, {bindings})
41
+ await profiles.update(key, {bindings})
41
42
  $profileKey(key)
42
43
  }
43
44
 
@@ -56,7 +57,7 @@ export const [CustomizeView, makeCustomizeElement] = shadowDuplex((options: {con
56
57
  }
57
58
 
58
59
  return html`
59
- <div class=plate theme-card>
60
+ <div part=plate>
60
61
  <header class=topbar>
61
62
  <div class=corner>
62
63
  ${ProfileSelectView(options, $profileKey(), $profileKey)}
@@ -10,7 +10,7 @@ export default css`@layer view {
10
10
  text-shadow: var(--text-shadow);
11
11
  }
12
12
 
13
- .deck {
13
+ [part="plate"] {
14
14
  padding: var(--pad);
15
15
  --tint: oklch(from var(--prime) l c h / 50%);
16
16
  }
@@ -1,7 +1,7 @@
1
1
 
2
2
  import {html} from "lit"
3
3
  import {ShinyTabs, TabsControl} from "@e280/shiny"
4
- import {shadowDuplex, useCss, useOnce} from "@e280/sly"
4
+ import {shadowDuplex, useCss, useName, useOnce} from "@e280/sly"
5
5
 
6
6
  import styleCss from "./style.css.js"
7
7
  import themeCss from "../theme.css.js"
@@ -9,13 +9,12 @@ import gameControllerSvg from "../../icons/akar/game-controller.svg.js"
9
9
  import settingsHorizontalSvg from "../../icons/akar/settings-horizontal.svg.js"
10
10
 
11
11
  import {Deck} from "../../deck/types.js"
12
- import {ControllersView} from "../controllers/view.js"
13
12
  import {CustomizeView} from "../customize/view.js"
13
+ import {ControllersView} from "../controllers/view.js"
14
14
 
15
15
  export const [DeckView, makeDeckElement] = shadowDuplex((deck: Deck) => {
16
+ useName("tact-deck")
16
17
  useCss(themeCss, styleCss)
17
-
18
- const attrs = {slot: "panel"}
19
18
  const tabsControl = useOnce(() => new TabsControl())
20
19
 
21
20
  // TODO remove this
@@ -25,7 +24,7 @@ export const [DeckView, makeDeckElement] = shadowDuplex((deck: Deck) => {
25
24
  })
26
25
 
27
26
  return html`
28
- <div class=deck theme-card>
27
+ <div part=plate theme-card>
29
28
  ${ShinyTabs.with({
30
29
  props: [{control: tabsControl}],
31
30
  children: html`
@@ -39,8 +38,15 @@ export const [DeckView, makeDeckElement] = shadowDuplex((deck: Deck) => {
39
38
  <span>customize</span>
40
39
  </button>
41
40
 
42
- ${ControllersView.with({attrs, props: [deck]})}
43
- ${CustomizeView.with({attrs, props: [deck]})}
41
+ ${ControllersView.with({
42
+ props: [deck],
43
+ attrs: {slot: "panel"},
44
+ })}
45
+
46
+ ${CustomizeView.with({
47
+ props: [deck],
48
+ attrs: {slot: "panel"},
49
+ })}
44
50
  `,
45
51
  })}
46
52
  </div>
@@ -82,7 +82,7 @@ ${cssReset}
82
82
  --base: var(--tint, var(--prime));
83
83
  font: inherit;
84
84
  color: inherit;
85
- background: transparent;
85
+ background: var(--bg);
86
86
  border: 1px solid var(--base);
87
87
  border-radius: var(--round);
88
88
  padding: var(--nib) var(--pad);