@benev/tact 0.4.0-6 → 0.4.0-8

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 (82) hide show
  1. package/README.md +95 -26
  2. package/package.json +3 -2
  3. package/s/lib/input/controller.ts +4 -1
  4. package/s/lib/input/data/intent/decode.ts +2 -2
  5. package/s/lib/input/data/intent/encode.ts +2 -2
  6. package/s/lib/input/data/intent/minihash.ts +5 -0
  7. package/s/lib/input/decoder.ts +2 -2
  8. package/s/lib/input/evaluate/context.ts +5 -1
  9. package/s/lib/input/evaluate/evaluate.ts +27 -14
  10. package/s/lib/input/investigate/hash-bindings.ts +5 -3
  11. package/s/lib/input/investigate/investigate.ts +24 -9
  12. package/s/lib/input/test.ts +52 -30
  13. package/s/lib/input/testing/quickbind.ts +3 -3
  14. package/s/lib/ui/deck/auto-gamepads.ts +1 -0
  15. package/s/lib/ui/deck/collector.ts +3 -3
  16. package/s/lib/ui/deck/hub.ts +4 -3
  17. package/s/lib/ui/deck/setup/deck.ts +33 -20
  18. package/s/lib/ui/deck/setup/tact.ts +20 -11
  19. package/s/lib/ui/deck/setup/types.ts +1 -0
  20. package/s/lib/ui/deck/setup/utils/broadcast-channel.ts +17 -0
  21. package/s/lib/ui/deck/setup/utils/kv.ts +20 -0
  22. package/s/lib/ui/deck/types.ts +1 -1
  23. package/s/lib/ui/views/customize/view.ts +3 -3
  24. package/s/lib/utils/hash.ts +47 -0
  25. package/x/demo/main.bundle.min.js +104 -104
  26. package/x/demo/main.bundle.min.js.map +4 -4
  27. package/x/gamepad/index.html +1 -1
  28. package/x/index.html +2 -2
  29. package/x/lib/input/controller.js +1 -1
  30. package/x/lib/input/controller.js.map +1 -1
  31. package/x/lib/input/data/intent/decode.d.ts +1 -1
  32. package/x/lib/input/data/intent/decode.js +2 -2
  33. package/x/lib/input/data/intent/decode.js.map +1 -1
  34. package/x/lib/input/data/intent/encode.d.ts +1 -1
  35. package/x/lib/input/data/intent/encode.js +2 -2
  36. package/x/lib/input/data/intent/encode.js.map +1 -1
  37. package/x/lib/input/data/intent/minihash.d.ts +1 -0
  38. package/x/lib/input/data/intent/minihash.js +4 -0
  39. package/x/lib/input/data/intent/minihash.js.map +1 -0
  40. package/x/lib/input/decoder.d.ts +1 -1
  41. package/x/lib/input/decoder.js +2 -2
  42. package/x/lib/input/decoder.js.map +1 -1
  43. package/x/lib/input/evaluate/context.d.ts +3 -1
  44. package/x/lib/input/evaluate/context.js.map +1 -1
  45. package/x/lib/input/evaluate/evaluate.js +24 -11
  46. package/x/lib/input/evaluate/evaluate.js.map +1 -1
  47. package/x/lib/input/investigate/hash-bindings.d.ts +1 -1
  48. package/x/lib/input/investigate/hash-bindings.js +5 -3
  49. package/x/lib/input/investigate/hash-bindings.js.map +1 -1
  50. package/x/lib/input/investigate/investigate.d.ts +2 -1
  51. package/x/lib/input/investigate/investigate.js +14 -9
  52. package/x/lib/input/investigate/investigate.js.map +1 -1
  53. package/x/lib/input/test.d.ts +4 -2
  54. package/x/lib/input/test.js +49 -22
  55. package/x/lib/input/test.js.map +1 -1
  56. package/x/lib/input/testing/quickbind.d.ts +3 -2
  57. package/x/lib/input/testing/quickbind.js +3 -3
  58. package/x/lib/input/testing/quickbind.js.map +1 -1
  59. package/x/lib/ui/deck/auto-gamepads.js +1 -0
  60. package/x/lib/ui/deck/auto-gamepads.js.map +1 -1
  61. package/x/lib/ui/deck/collector.js +2 -2
  62. package/x/lib/ui/deck/collector.js.map +1 -1
  63. package/x/lib/ui/deck/hub.js +4 -3
  64. package/x/lib/ui/deck/hub.js.map +1 -1
  65. package/x/lib/ui/deck/setup/deck.js +23 -13
  66. package/x/lib/ui/deck/setup/deck.js.map +1 -1
  67. package/x/lib/ui/deck/setup/tact.d.ts +2 -2
  68. package/x/lib/ui/deck/setup/tact.js +15 -8
  69. package/x/lib/ui/deck/setup/tact.js.map +1 -1
  70. package/x/lib/ui/deck/setup/types.d.ts +1 -0
  71. package/x/lib/ui/deck/setup/utils/broadcast-channel.d.ts +4 -0
  72. package/x/lib/ui/deck/setup/utils/broadcast-channel.js +10 -0
  73. package/x/lib/ui/deck/setup/utils/broadcast-channel.js.map +1 -0
  74. package/x/lib/ui/deck/setup/utils/kv.d.ts +5 -0
  75. package/x/lib/ui/deck/setup/utils/kv.js +12 -0
  76. package/x/lib/ui/deck/setup/utils/kv.js.map +1 -0
  77. package/x/lib/ui/deck/types.d.ts +1 -0
  78. package/x/lib/ui/views/customize/view.js +3 -3
  79. package/x/lib/ui/views/customize/view.js.map +1 -1
  80. package/x/lib/utils/hash.d.ts +1 -0
  81. package/x/lib/utils/hash.js +47 -0
  82. package/x/lib/utils/hash.js.map +1 -0
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,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` to 1 for single player.*
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.deck.hub.encodeAll()
72
- for (const {playerId, inputs} of tact.collector.decodeAll(buffer)) {
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 to 1.
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
- - 💡 *maybe imagine streaming the buffers across the network to a remote collector.*
85
- - 💡 *maybe imagine 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!).*
86
95
  - 🍏 **put the `<tact-deck>` element in your html page.**
87
96
  ```html
88
97
  <tact-deck></tact-deck>
89
98
  ```
90
- - 💡 *you also have each panel, `<tact-controllers>` and `<tact-customize>`.*
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,15 +112,15 @@ 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
- > *tact is flexible and composable for alternative use-cases.*
115
+ > *for simpler apps, you can use tact's composable parts more directly.*
108
116
 
109
- - 🍏 **pov: your app isn't a game, no multiplayer, no gamepads.**
117
+ - ⚙️ **pov: your app isn't a game, no multiplayer, no gamepads.**
110
118
  ```ts
111
- import {setupTact, InputDecoder} from "@benev/tact"
119
+ import {setupTact, Decoder} from "@benev/tact"
112
120
 
113
121
  const tact = await setupTact({presets, maxPlayers: 1})
114
122
  const controller = tact.mouseKeyboard()
115
- const decoder = new InputDecoder(bindings)
123
+ const decoder = new Decoder(bindings)
116
124
  tact.registerElements()
117
125
 
118
126
  setInterval(() => {
@@ -124,13 +132,13 @@ 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
- - 🍏 **pov: your app doesn't even want any ui, or persistence.**
135
+ - ⚙️ **pov: your app doesn't even want any ui, or persistence.**
128
136
  ```ts
129
- import {Controller, InputDecoder, KeyboardSource, PointerSource, Sources} from "@benev/tact"
137
+ import {Controller, Decoder, KeyboardSource, PointerSource, Sources} from "@benev/tact"
130
138
 
131
139
  const source = new Sources(new KeyboardSource(), new PointerSource())
132
140
  const controller = new Controller({bindings, source})
133
- const decoder = new InputDecoder(bindings)
141
+ const decoder = new Decoder(bindings)
134
142
 
135
143
  setInterval(() => {
136
144
  const inputs = decoder.decode(controller.encode())
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@benev/tact",
3
- "version": "0.4.0-6",
3
+ "version": "0.4.0-8",
4
4
  "description": "🎮 from keypress to couch co-op",
5
5
  "license": "MIT",
6
6
  "author": "Chase Moskal <chasemoskal@gmail.com>",
@@ -26,6 +26,7 @@
26
26
  "lit": "^3.3.3"
27
27
  },
28
28
  "dependencies": {
29
+ "@awasm/noble": "^0.1.4",
29
30
  "@benev/math": "0.4.0-9",
30
31
  "@e280/kv": "^0.2.4",
31
32
  "@e280/shiny": "^0.1.1",
@@ -35,7 +36,7 @@
35
36
  "@e280/hottie": "^0.1.7",
36
37
  "@e280/octo": "^0.1.7",
37
38
  "@e280/science": "^0.1.14",
38
- "@e280/scute": "^0.5.4",
39
+ "@e280/scute": "^0.5.5",
39
40
  "http-server": "^14.1.1",
40
41
  "npm-run-all": "^4.1.5",
41
42
  "typescript": "^7.0.2"
@@ -58,7 +58,10 @@ export class Controller<B extends Bindings = any> {
58
58
  }
59
59
 
60
60
  encode(time = performance.now()) {
61
- return encodeIntents(this.#investigation.hash, [...this.evaluate(time)])
61
+ return encodeIntents(
62
+ this.#investigation.minihash,
63
+ [...this.evaluate(time)],
64
+ )
62
65
  }
63
66
 
64
67
  clear() {
@@ -8,7 +8,7 @@ export function decodeIntents(buffer: ArrayBuffer) {
8
8
  const dv = new DataView(buffer)
9
9
  let cursor = 0
10
10
 
11
- const hash = dv.getUint32(cursor, endian)
11
+ const minihash = dv.getUint32(cursor, endian)
12
12
  cursor += sizeHash
13
13
 
14
14
  while (cursor < buffer.byteLength) {
@@ -21,6 +21,6 @@ export function decodeIntents(buffer: ArrayBuffer) {
21
21
  intents.push({id, value})
22
22
  }
23
23
 
24
- return {hash, intents}
24
+ return {minihash, intents}
25
25
  }
26
26
 
@@ -3,13 +3,13 @@ import {Intent} from "./types.js"
3
3
  import {endian} from "../endian.js"
4
4
  import {sizeHash, sizeId, sizeIntent, sizeValue} from "./sizes.js"
5
5
 
6
- export function encodeIntents(hash: number, intents: Intent[]) {
6
+ export function encodeIntents(minihash: number, intents: Intent[]) {
7
7
  const byteCount = sizeHash + (intents.length * sizeIntent)
8
8
  const buffer = new ArrayBuffer(byteCount)
9
9
  const dv = new DataView(buffer)
10
10
  let cursor = 0
11
11
 
12
- dv.setUint32(cursor, hash, endian)
12
+ dv.setUint32(cursor, minihash, endian)
13
13
  cursor += sizeHash
14
14
 
15
15
  for (const intent of intents) {
@@ -0,0 +1,5 @@
1
+
2
+ export function minihash(hash: string) {
3
+ return Number.parseInt(hash.slice(0, 8), 16)
4
+ }
5
+
@@ -3,7 +3,7 @@ import {Inputs, Bindings} from "./types.js"
3
3
  import {decodeIntents} from "./data/intent/decode.js"
4
4
  import {investigate} from "./investigate/investigate.js"
5
5
 
6
- export class InputDecoder<B extends Bindings> {
6
+ export class Decoder<B extends Bindings> {
7
7
  #investigation
8
8
 
9
9
  constructor(bindings: B) {
@@ -25,7 +25,7 @@ export class InputDecoder<B extends Bindings> {
25
25
  for (const buffer of buffers) {
26
26
  const decoded = decodeIntents(buffer)
27
27
 
28
- if (decoded.hash !== this.#investigation.hash)
28
+ if (decoded.minihash !== this.#investigation.minihash)
29
29
  throw new Error("intent bindings hash mismatch")
30
30
 
31
31
  for (const input of inputs)
@@ -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": {
@@ -1,6 +1,7 @@
1
1
 
2
- import {hash32} from "@e280/stz"
2
+ import {hex} from "@e280/stz"
3
3
  import {Bindings} from "../types.js"
4
+ import {hash} from "../../utils/hash.js"
4
5
  import {isExpression} from "./is-expression.js"
5
6
 
6
7
  export function hashBindings(bindings: Bindings) {
@@ -8,7 +9,7 @@ export function hashBindings(bindings: Bindings) {
8
9
 
9
10
  function recurse(b: Bindings, path: string[]) {
10
11
  if (isExpression(b)) {
11
- paths.push(path.join("."))
12
+ paths.push(JSON.stringify(path))
12
13
  }
13
14
  else {
14
15
  for (const [key, value] of Object.entries(b))
@@ -17,6 +18,7 @@ export function hashBindings(bindings: Bindings) {
17
18
  }
18
19
 
19
20
  recurse(bindings, ["b"])
20
- return hash32(...paths)
21
+ paths.sort()
22
+ return hex(hash("tact/bindings/v1", ...paths).slice(16))
21
23
  }
22
24
 
@@ -5,31 +5,46 @@ import {Expression} from "../expression.js"
5
5
  import {hashBindings} from "./hash-bindings.js"
6
6
  import {isExpression} from "./is-expression.js"
7
7
  import {Bindings, Input, Inputs} from "../types.js"
8
+ import {minihash} from "../data/intent/minihash.js"
8
9
 
9
10
  export function investigate<B extends Bindings>(bindings: B) {
10
11
  const hash = hashBindings(bindings)
11
- const inputList: Input[] = []
12
- const rootList: Expression[] = []
13
12
 
14
- function makeInputs(b: Bindings): unknown {
13
+ const leaves: {
14
+ path: string
15
+ input: Input
16
+ expression: Expression
17
+ }[] = []
18
+
19
+ function makeInputs(b: Bindings, path: string[]): unknown {
15
20
  if (isExpression(b)) {
16
21
  const input = makeInput()
17
- inputList.push(input)
18
- rootList.push(b)
22
+ leaves.push({
23
+ path: JSON.stringify(path),
24
+ input,
25
+ expression: b,
26
+ })
19
27
  return input
20
28
  }
21
29
  else {
22
- return obmap(b, makeInputs)
30
+ return obmap(b, (value, key) =>
31
+ makeInputs(value, [...path, key as string])
32
+ )
23
33
  }
24
34
  }
25
35
 
26
- const inputs = makeInputs(bindings) as Inputs<B>
36
+ const inputs = makeInputs(bindings, ["b"]) as Inputs<B>
37
+
38
+ leaves.sort((a, b) =>
39
+ a.path < b.path ? -1 : a.path > b.path ? 1 : 0
40
+ )
27
41
 
28
42
  return {
29
43
  hash,
44
+ minihash: minihash(hash),
30
45
  inputs,
31
- inputList,
32
- rootList,
46
+ inputList: leaves.map(l => l.input),
47
+ rootList: leaves.map(l => l.expression),
33
48
  }
34
49
  }
35
50
 
@@ -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
  }
@@ -4,12 +4,12 @@ import {guarantee} from "@e280/stz"
4
4
 
5
5
  import {PlayerId} from "./types.js"
6
6
  import {Bindings} from "../../input/types.js"
7
- import {InputDecoder} from "../../input/decoder.js"
7
+ import {Decoder} from "../../input/decoder.js"
8
8
  import {decodeParcels} from "../../input/data/parcel/decode.js"
9
9
 
10
10
  export class Collector<B extends Bindings> {
11
11
  #bindings: B
12
- #decoders = new RMap<PlayerId, InputDecoder<B>>()
12
+ #decoders = new RMap<PlayerId, Decoder<B>>()
13
13
 
14
14
  constructor(bindings: B) {
15
15
  this.#bindings = bindings
@@ -26,7 +26,7 @@ export class Collector<B extends Bindings> {
26
26
  return guarantee(
27
27
  this.#decoders,
28
28
  playerId,
29
- () => new InputDecoder(this.#bindings),
29
+ () => new Decoder(this.#bindings),
30
30
  )
31
31
  }
32
32
 
@@ -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()) {